home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / date.php < prev    next >
Encoding:
PHP Script  |  2017-07-26  |  34.2 KB  |  1,002 lines

  1. <?php
  2. /**
  3.  * Class for generating SQL clauses that filter a primary query according to date.
  4.  *
  5.  * WP_Date_Query is a helper that allows primary query classes, such as WP_Query, to filter
  6.  * their results by date columns, by generating `WHERE` subclauses to be attached to the
  7.  * primary SQL query string.
  8.  *
  9.  * Attempting to filter by an invalid date value (eg month=13) will generate SQL that will
  10.  * return no results. In these cases, a _doing_it_wrong() error notice is also thrown.
  11.  * See WP_Date_Query::validate_date_values().
  12.  *
  13.  * @link https://codex.wordpress.org/Function_Reference/WP_Query Codex page.
  14.  *
  15.  * @since 3.7.0
  16.  */
  17. class WP_Date_Query {
  18.     /**
  19.      * Array of date queries.
  20.      *
  21.      * See WP_Date_Query::__construct() for information on date query arguments.
  22.      *
  23.      * @since 3.7.0
  24.      * @var array
  25.      */
  26.     public $queries = array();
  27.  
  28.     /**
  29.      * The default relation between top-level queries. Can be either 'AND' or 'OR'.
  30.      *
  31.      * @since 3.7.0
  32.      * @var string
  33.      */
  34.     public $relation = 'AND';
  35.  
  36.     /**
  37.      * The column to query against. Can be changed via the query arguments.
  38.      *
  39.      * @since 3.7.0
  40.      * @var string
  41.      */
  42.     public $column = 'post_date';
  43.  
  44.     /**
  45.      * The value comparison operator. Can be changed via the query arguments.
  46.      *
  47.      * @since 3.7.0
  48.      * @var array
  49.      */
  50.     public $compare = '=';
  51.  
  52.     /**
  53.      * Supported time-related parameter keys.
  54.      *
  55.      * @since 4.1.0
  56.      * @var array
  57.      */
  58.     public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' );
  59.  
  60.     /**
  61.      * Constructor.
  62.      *
  63.      * Time-related parameters that normally require integer values ('year', 'month', 'week', 'dayofyear', 'day',
  64.      * 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second') accept arrays of integers for some values of
  65.      * 'compare'. When 'compare' is 'IN' or 'NOT IN', arrays are accepted; when 'compare' is 'BETWEEN' or 'NOT
  66.      * BETWEEN', arrays of two valid values are required. See individual argument descriptions for accepted values.
  67.      *
  68.      * @since 3.7.0
  69.      * @since 4.0.0 The $inclusive logic was updated to include all times within the date range.
  70.      * @since 4.1.0 Introduced 'dayofweek_iso' time type parameter.
  71.      *
  72.      * @param array $date_query {
  73.      *     Array of date query clauses.
  74.      *
  75.      *     @type array {
  76.      *         @type string $column   Optional. The column to query against. If undefined, inherits the value of
  77.      *                                the `$default_column` parameter. Accepts 'post_date', 'post_date_gmt',
  78.      *                                'post_modified','post_modified_gmt', 'comment_date', 'comment_date_gmt'.
  79.      *                                Default 'post_date'.
  80.      *         @type string $compare  Optional. The comparison operator. Accepts '=', '!=', '>', '>=', '<', '<=',
  81.      *                                'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. Default '='.
  82.      *         @type string $relation Optional. The boolean relationship between the date queries. Accepts 'OR' or 'AND'.
  83.      *                                Default 'OR'.
  84.      *         @type array {
  85.      *             Optional. An array of first-order clause parameters, or another fully-formed date query.
  86.      *
  87.      *             @type string|array $before {
  88.      *                 Optional. Date to retrieve posts before. Accepts `strtotime()`-compatible string,
  89.      *                 or array of 'year', 'month', 'day' values.
  90.      *
  91.      *                 @type string $year  The four-digit year. Default empty. Accepts any four-digit year.
  92.      *                 @type string $month Optional when passing array.The month of the year.
  93.      *                                     Default (string:empty)|(array:1). Accepts numbers 1-12.
  94.      *                 @type string $day   Optional when passing array.The day of the month.
  95.      *                                     Default (string:empty)|(array:1). Accepts numbers 1-31.
  96.      *             }
  97.      *             @type string|array $after {
  98.      *                 Optional. Date to retrieve posts after. Accepts `strtotime()`-compatible string,
  99.      *                 or array of 'year', 'month', 'day' values.
  100.      *
  101.      *                 @type string $year  The four-digit year. Accepts any four-digit year. Default empty.
  102.      *                 @type string $month Optional when passing array. The month of the year. Accepts numbers 1-12.
  103.      *                                     Default (string:empty)|(array:12).
  104.      *                 @type string $day   Optional when passing array.The day of the month. Accepts numbers 1-31.
  105.      *                                     Default (string:empty)|(array:last day of month).
  106.      *             }
  107.      *             @type string       $column        Optional. Used to add a clause comparing a column other than the
  108.      *                                               column specified in the top-level `$column` parameter. Accepts
  109.      *                                               'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt',
  110.      *                                               'comment_date', 'comment_date_gmt'. Default is the value of
  111.      *                                               top-level `$column`.
  112.      *             @type string       $compare       Optional. The comparison operator. Accepts '=', '!=', '>', '>=',
  113.      *                                               '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. 'IN',
  114.      *                                               'NOT IN', 'BETWEEN', and 'NOT BETWEEN'. Comparisons support
  115.      *                                               arrays in some time-related parameters. Default '='.
  116.      *             @type bool         $inclusive     Optional. Include results from dates specified in 'before' or
  117.      *                                               'after'. Default false.
  118.      *             @type int|array    $year          Optional. The four-digit year number. Accepts any four-digit year
  119.      *                                               or an array of years if `$compare` supports it. Default empty.
  120.      *             @type int|array    $month         Optional. The two-digit month number. Accepts numbers 1-12 or an
  121.      *                                               array of valid numbers if `$compare` supports it. Default empty.
  122.      *             @type int|array    $week          Optional. The week number of the year. Accepts numbers 0-53 or an
  123.      *                                               array of valid numbers if `$compare` supports it. Default empty.
  124.      *             @type int|array    $dayofyear     Optional. The day number of the year. Accepts numbers 1-366 or an
  125.      *                                               array of valid numbers if `$compare` supports it.
  126.      *             @type int|array    $day           Optional. The day of the month. Accepts numbers 1-31 or an array
  127.      *                                               of valid numbers if `$compare` supports it. Default empty.
  128.      *             @type int|array    $dayofweek     Optional. The day number of the week. Accepts numbers 1-7 (1 is
  129.      *                                               Sunday) or an array of valid numbers if `$compare` supports it.
  130.      *                                               Default empty.
  131.      *             @type int|array    $dayofweek_iso Optional. The day number of the week (ISO). Accepts numbers 1-7
  132.      *                                               (1 is Monday) or an array of valid numbers if `$compare` supports it.
  133.      *                                               Default empty.
  134.      *             @type int|array    $hour          Optional. The hour of the day. Accepts numbers 0-23 or an array
  135.      *                                               of valid numbers if `$compare` supports it. Default empty.
  136.      *             @type int|array    $minute        Optional. The minute of the hour. Accepts numbers 0-60 or an array
  137.      *                                               of valid numbers if `$compare` supports it. Default empty.
  138.      *             @type int|array    $second        Optional. The second of the minute. Accepts numbers 0-60 or an
  139.      *                                               array of valid numbers if `$compare` supports it. Default empty.
  140.      *         }
  141.      *     }
  142.      * }
  143.      * @param array $default_column Optional. Default column to query against. Default 'post_date'.
  144.      *                              Accepts 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt',
  145.      *                              'comment_date', 'comment_date_gmt'.
  146.      */
  147.     public function __construct( $date_query, $default_column = 'post_date' ) {
  148.         if ( isset( $date_query['relation'] ) && 'OR' === strtoupper( $date_query['relation'] ) ) {
  149.             $this->relation = 'OR';
  150.         } else {
  151.             $this->relation = 'AND';
  152.         }
  153.  
  154.         if ( ! is_array( $date_query ) ) {
  155.             return;
  156.         }
  157.  
  158.         // Support for passing time-based keys in the top level of the $date_query array.
  159.         if ( ! isset( $date_query[0] ) && ! empty( $date_query ) ) {
  160.             $date_query = array( $date_query );
  161.         }
  162.  
  163.         if ( empty( $date_query ) ) {
  164.             return;
  165.         }
  166.  
  167.         if ( ! empty( $date_query['column'] ) ) {
  168.             $date_query['column'] = esc_sql( $date_query['column'] );
  169.         } else {
  170.             $date_query['column'] = esc_sql( $default_column );
  171.         }
  172.  
  173.         $this->column = $this->validate_column( $this->column );
  174.  
  175.         $this->compare = $this->get_compare( $date_query );
  176.  
  177.         $this->queries = $this->sanitize_query( $date_query );
  178.     }
  179.  
  180.     /**
  181.      * Recursive-friendly query sanitizer.
  182.      *
  183.      * Ensures that each query-level clause has a 'relation' key, and that
  184.      * each first-order clause contains all the necessary keys from
  185.      * `$defaults`.
  186.      *
  187.      * @since 4.1.0
  188.      *
  189.      * @param array $queries
  190.      * @param array $parent_query
  191.      *
  192.      * @return array Sanitized queries.
  193.      */
  194.     public function sanitize_query( $queries, $parent_query = null ) {
  195.         $cleaned_query = array();
  196.  
  197.         $defaults = array(
  198.             'column'   => 'post_date',
  199.             'compare'  => '=',
  200.             'relation' => 'AND',
  201.         );
  202.  
  203.         // Numeric keys should always have array values.
  204.         foreach ( $queries as $qkey => $qvalue ) {
  205.             if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) {
  206.                 unset( $queries[ $qkey ] );
  207.             }
  208.         }
  209.  
  210.         // Each query should have a value for each default key. Inherit from the parent when possible.
  211.         foreach ( $defaults as $dkey => $dvalue ) {
  212.             if ( isset( $queries[ $dkey ] ) ) {
  213.                 continue;
  214.             }
  215.  
  216.             if ( isset( $parent_query[ $dkey ] ) ) {
  217.                 $queries[ $dkey ] = $parent_query[ $dkey ];
  218.             } else {
  219.                 $queries[ $dkey ] = $dvalue;
  220.             }
  221.         }
  222.  
  223.         // Validate the dates passed in the query.
  224.         if ( $this->is_first_order_clause( $queries ) ) {
  225.             $this->validate_date_values( $queries );
  226.         }
  227.  
  228.         foreach ( $queries as $key => $q ) {
  229.             if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) {
  230.                 // This is a first-order query. Trust the values and sanitize when building SQL.
  231.                 $cleaned_query[ $key ] = $q;
  232.             } else {
  233.                 // Any array without a time key is another query, so we recurse.
  234.                 $cleaned_query[] = $this->sanitize_query( $q, $queries );
  235.             }
  236.         }
  237.  
  238.         return $cleaned_query;
  239.     }
  240.  
  241.     /**
  242.      * Determine whether this is a first-order clause.
  243.      *
  244.      * Checks to see if the current clause has any time-related keys.
  245.      * If so, it's first-order.
  246.      *
  247.      * @since 4.1.0
  248.      *
  249.      * @param  array $query Query clause.
  250.      * @return bool True if this is a first-order clause.
  251.      */
  252.     protected function is_first_order_clause( $query ) {
  253.         $time_keys = array_intersect( $this->time_keys, array_keys( $query ) );
  254.         return ! empty( $time_keys );
  255.     }
  256.  
  257.     /**
  258.      * Determines and validates what comparison operator to use.
  259.      *
  260.      * @since 3.7.0
  261.      *
  262.      * @param array $query A date query or a date subquery.
  263.      * @return string The comparison operator.
  264.      */
  265.     public function get_compare( $query ) {
  266.         if ( ! empty( $query['compare'] ) && in_array( $query['compare'], array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) )
  267.             return strtoupper( $query['compare'] );
  268.  
  269.         return $this->compare;
  270.     }
  271.  
  272.     /**
  273.      * Validates the given date_query values and triggers errors if something is not valid.
  274.      *
  275.      * Note that date queries with invalid date ranges are allowed to
  276.      * continue (though of course no items will be found for impossible dates).
  277.      * This method only generates debug notices for these cases.
  278.      *
  279.      * @since  4.1.0
  280.      *
  281.      * @param  array $date_query The date_query array.
  282.      * @return bool  True if all values in the query are valid, false if one or more fail.
  283.      */
  284.     public function validate_date_values( $date_query = array() ) {
  285.         if ( empty( $date_query ) ) {
  286.             return false;
  287.         }
  288.  
  289.         $valid = true;
  290.  
  291.         /*
  292.          * Validate 'before' and 'after' up front, then let the
  293.          * validation routine continue to be sure that all invalid
  294.          * values generate errors too.
  295.          */
  296.         if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ){
  297.             $valid = $this->validate_date_values( $date_query['before'] );
  298.         }
  299.  
  300.         if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ){
  301.             $valid = $this->validate_date_values( $date_query['after'] );
  302.         }
  303.  
  304.         // Array containing all min-max checks.
  305.         $min_max_checks = array();
  306.  
  307.         // Days per year.
  308.         if ( array_key_exists( 'year', $date_query ) ) {
  309.             /*
  310.              * If a year exists in the date query, we can use it to get the days.
  311.              * If multiple years are provided (as in a BETWEEN), use the first one.
  312.              */
  313.             if ( is_array( $date_query['year'] ) ) {
  314.                 $_year = reset( $date_query['year'] );
  315.             } else {
  316.                 $_year = $date_query['year'];
  317.             }
  318.  
  319.             $max_days_of_year = date( 'z', mktime( 0, 0, 0, 12, 31, $_year ) ) + 1;
  320.         } else {
  321.             // otherwise we use the max of 366 (leap-year)
  322.             $max_days_of_year = 366;
  323.         }
  324.  
  325.         $min_max_checks['dayofyear'] = array(
  326.             'min' => 1,
  327.             'max' => $max_days_of_year
  328.         );
  329.  
  330.         // Days per week.
  331.         $min_max_checks['dayofweek'] = array(
  332.             'min' => 1,
  333.             'max' => 7
  334.         );
  335.  
  336.         // Days per week.
  337.         $min_max_checks['dayofweek_iso'] = array(
  338.             'min' => 1,
  339.             'max' => 7
  340.         );
  341.  
  342.         // Months per year.
  343.         $min_max_checks['month'] = array(
  344.             'min' => 1,
  345.             'max' => 12
  346.         );
  347.  
  348.         // Weeks per year.
  349.         if ( isset( $_year ) ) {
  350.             /*
  351.              * If we have a specific year, use it to calculate number of weeks.
  352.              * Note: the number of weeks in a year is the date in which Dec 28 appears.
  353.              */
  354.             $week_count = date( 'W', mktime( 0, 0, 0, 12, 28, $_year ) );
  355.  
  356.         } else {
  357.             // Otherwise set the week-count to a maximum of 53.
  358.             $week_count = 53;
  359.         }
  360.  
  361.         $min_max_checks['week'] = array(
  362.             'min' => 1,
  363.             'max' => $week_count
  364.         );
  365.  
  366.         // Days per month.
  367.         $min_max_checks['day'] = array(
  368.             'min' => 1,
  369.             'max' => 31
  370.         );
  371.  
  372.         // Hours per day.
  373.         $min_max_checks['hour'] = array(
  374.             'min' => 0,
  375.             'max' => 23
  376.         );
  377.  
  378.         // Minutes per hour.
  379.         $min_max_checks['minute'] = array(
  380.             'min' => 0,
  381.             'max' => 59
  382.         );
  383.  
  384.         // Seconds per minute.
  385.         $min_max_checks['second'] = array(
  386.             'min' => 0,
  387.             'max' => 59
  388.         );
  389.  
  390.         // Concatenate and throw a notice for each invalid value.
  391.         foreach ( $min_max_checks as $key => $check ) {
  392.             if ( ! array_key_exists( $key, $date_query ) ) {
  393.                 continue;
  394.             }
  395.  
  396.             // Throw a notice for each failing value.
  397.             foreach ( (array) $date_query[ $key ] as $_value ) {
  398.                 $is_between = $_value >= $check['min'] && $_value <= $check['max'];
  399.  
  400.                 if ( ! is_numeric( $_value ) || ! $is_between ) {
  401.                     $error = sprintf(
  402.                         /* translators: Date query invalid date message: 1: invalid value, 2: type of value, 3: minimum valid value, 4: maximum valid value */
  403.                         __( 'Invalid value %1$s for %2$s. Expected value should be between %3$s and %4$s.' ),
  404.                         '<code>' . esc_html( $_value ) . '</code>',
  405.                         '<code>' . esc_html( $key ) . '</code>',
  406.                         '<code>' . esc_html( $check['min'] ) . '</code>',
  407.                         '<code>' . esc_html( $check['max'] ) . '</code>'
  408.                     );
  409.  
  410.                     _doing_it_wrong( __CLASS__, $error, '4.1.0' );
  411.  
  412.                     $valid = false;
  413.                 }
  414.             }
  415.         }
  416.  
  417.         // If we already have invalid date messages, don't bother running through checkdate().
  418.         if ( ! $valid ) {
  419.             return $valid;
  420.         }
  421.  
  422.         $day_month_year_error_msg = '';
  423.  
  424.         $day_exists   = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] );
  425.         $month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] );
  426.         $year_exists  = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] );
  427.  
  428.         if ( $day_exists && $month_exists && $year_exists ) {
  429.             // 1. Checking day, month, year combination.
  430.             if ( ! wp_checkdate( $date_query['month'], $date_query['day'], $date_query['year'], sprintf( '%s-%s-%s', $date_query['year'], $date_query['month'], $date_query['day'] ) ) ) {
  431.                 /* translators: 1: year, 2: month, 3: day of month */
  432.                 $day_month_year_error_msg = sprintf(
  433.                     __( 'The following values do not describe a valid date: year %1$s, month %2$s, day %3$s.' ),
  434.                     '<code>' . esc_html( $date_query['year'] ) . '</code>',
  435.                     '<code>' . esc_html( $date_query['month'] ) . '</code>',
  436.                     '<code>' . esc_html( $date_query['day'] ) . '</code>'
  437.                 );
  438.  
  439.                 $valid = false;
  440.             }
  441.  
  442.         } elseif ( $day_exists && $month_exists ) {
  443.             /*
  444.              * 2. checking day, month combination
  445.              * We use 2012 because, as a leap year, it's the most permissive.
  446.              */
  447.             if ( ! wp_checkdate( $date_query['month'], $date_query['day'], 2012, sprintf( '2012-%s-%s', $date_query['month'], $date_query['day'] ) ) ) {
  448.                 /* translators: 1: month, 2: day of month */
  449.                 $day_month_year_error_msg = sprintf(
  450.                     __( 'The following values do not describe a valid date: month %1$s, day %2$s.' ),
  451.                     '<code>' . esc_html( $date_query['month'] ) . '</code>',
  452.                     '<code>' . esc_html( $date_query['day'] ) . '</code>'
  453.                 );
  454.  
  455.                 $valid = false;
  456.             }
  457.         }
  458.  
  459.         if ( ! empty( $day_month_year_error_msg ) ) {
  460.             _doing_it_wrong( __CLASS__, $day_month_year_error_msg, '4.1.0' );
  461.         }
  462.  
  463.         return $valid;
  464.     }
  465.  
  466.     /**
  467.      * Validates a column name parameter.
  468.      *
  469.      * Column names without a table prefix (like 'post_date') are checked against a whitelist of
  470.      * known tables, and then, if found, have a table prefix (such as 'wp_posts.') prepended.
  471.      * Prefixed column names (such as 'wp_posts.post_date') bypass this whitelist check,
  472.      * and are only sanitized to remove illegal characters.
  473.      *
  474.      * @since 3.7.0
  475.      *
  476.      * @param string $column The user-supplied column name.
  477.      * @return string A validated column name value.
  478.      */
  479.     public function validate_column( $column ) {
  480.         global $wpdb;
  481.  
  482.         $valid_columns = array(
  483.             'post_date', 'post_date_gmt', 'post_modified',
  484.             'post_modified_gmt', 'comment_date', 'comment_date_gmt',
  485.             'user_registered', 'registered', 'last_updated',
  486.         );
  487.  
  488.         // Attempt to detect a table prefix.
  489.         if ( false === strpos( $column, '.' ) ) {
  490.             /**
  491.              * Filters the list of valid date query columns.
  492.              *
  493.              * @since 3.7.0
  494.              * @since 4.1.0 Added 'user_registered' to the default recognized columns.
  495.              *
  496.              * @param array $valid_columns An array of valid date query columns. Defaults
  497.              *                             are 'post_date', 'post_date_gmt', 'post_modified',
  498.              *                             'post_modified_gmt', 'comment_date', 'comment_date_gmt',
  499.              *                               'user_registered'
  500.              */
  501.             if ( ! in_array( $column, apply_filters( 'date_query_valid_columns', $valid_columns ) ) ) {
  502.                 $column = 'post_date';
  503.             }
  504.  
  505.             $known_columns = array(
  506.                 $wpdb->posts => array(
  507.                     'post_date',
  508.                     'post_date_gmt',
  509.                     'post_modified',
  510.                     'post_modified_gmt',
  511.                 ),
  512.                 $wpdb->comments => array(
  513.                     'comment_date',
  514.                     'comment_date_gmt',
  515.                 ),
  516.                 $wpdb->users => array(
  517.                     'user_registered',
  518.                 ),
  519.                 $wpdb->blogs => array(
  520.                     'registered',
  521.                     'last_updated',
  522.                 ),
  523.             );
  524.  
  525.             // If it's a known column name, add the appropriate table prefix.
  526.             foreach ( $known_columns as $table_name => $table_columns ) {
  527.                 if ( in_array( $column, $table_columns ) ) {
  528.                     $column = $table_name . '.' . $column;
  529.                     break;
  530.                 }
  531.             }
  532.  
  533.         }
  534.  
  535.         // Remove unsafe characters.
  536.         return preg_replace( '/[^a-zA-Z0-9_$\.]/', '', $column );
  537.     }
  538.  
  539.     /**
  540.      * Generate WHERE clause to be appended to a main query.
  541.      *
  542.      * @since 3.7.0
  543.      *
  544.      * @return string MySQL WHERE clause.
  545.      */
  546.     public function get_sql() {
  547.         $sql = $this->get_sql_clauses();
  548.  
  549.         $where = $sql['where'];
  550.  
  551.         /**
  552.          * Filters the date query WHERE clause.
  553.          *
  554.          * @since 3.7.0
  555.          *
  556.          * @param string        $where WHERE clause of the date query.
  557.          * @param WP_Date_Query $this  The WP_Date_Query instance.
  558.          */
  559.         return apply_filters( 'get_date_sql', $where, $this );
  560.     }
  561.  
  562.     /**
  563.      * Generate SQL clauses to be appended to a main query.
  564.      *
  565.      * Called by the public WP_Date_Query::get_sql(), this method is abstracted
  566.      * out to maintain parity with the other Query classes.
  567.      *
  568.      * @since 4.1.0
  569.      *
  570.      * @return array {
  571.      *     Array containing JOIN and WHERE SQL clauses to append to the main query.
  572.      *
  573.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  574.      *     @type string $where SQL fragment to append to the main WHERE clause.
  575.      * }
  576.      */
  577.     protected function get_sql_clauses() {
  578.         $sql = $this->get_sql_for_query( $this->queries );
  579.  
  580.         if ( ! empty( $sql['where'] ) ) {
  581.             $sql['where'] = ' AND ' . $sql['where'];
  582.         }
  583.  
  584.         return $sql;
  585.     }
  586.  
  587.     /**
  588.      * Generate SQL clauses for a single query array.
  589.      *
  590.      * If nested subqueries are found, this method recurses the tree to
  591.      * produce the properly nested SQL.
  592.      *
  593.      * @since 4.1.0
  594.      *
  595.      * @param array $query Query to parse.
  596.      * @param int   $depth Optional. Number of tree levels deep we currently are.
  597.      *                     Used to calculate indentation. Default 0.
  598.      * @return array {
  599.      *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
  600.      *
  601.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  602.      *     @type string $where SQL fragment to append to the main WHERE clause.
  603.      * }
  604.      */
  605.     protected function get_sql_for_query( $query, $depth = 0 ) {
  606.         $sql_chunks = array(
  607.             'join'  => array(),
  608.             'where' => array(),
  609.         );
  610.  
  611.         $sql = array(
  612.             'join'  => '',
  613.             'where' => '',
  614.         );
  615.  
  616.         $indent = '';
  617.         for ( $i = 0; $i < $depth; $i++ ) {
  618.             $indent .= "  ";
  619.         }
  620.  
  621.         foreach ( $query as $key => $clause ) {
  622.             if ( 'relation' === $key ) {
  623.                 $relation = $query['relation'];
  624.             } elseif ( is_array( $clause ) ) {
  625.  
  626.                 // This is a first-order clause.
  627.                 if ( $this->is_first_order_clause( $clause ) ) {
  628.                     $clause_sql = $this->get_sql_for_clause( $clause, $query );
  629.  
  630.                     $where_count = count( $clause_sql['where'] );
  631.                     if ( ! $where_count ) {
  632.                         $sql_chunks['where'][] = '';
  633.                     } elseif ( 1 === $where_count ) {
  634.                         $sql_chunks['where'][] = $clause_sql['where'][0];
  635.                     } else {
  636.                         $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
  637.                     }
  638.  
  639.                     $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
  640.                 // This is a subquery, so we recurse.
  641.                 } else {
  642.                     $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
  643.  
  644.                     $sql_chunks['where'][] = $clause_sql['where'];
  645.                     $sql_chunks['join'][]  = $clause_sql['join'];
  646.                 }
  647.             }
  648.         }
  649.  
  650.         // Filter to remove empties.
  651.         $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
  652.         $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
  653.  
  654.         if ( empty( $relation ) ) {
  655.             $relation = 'AND';
  656.         }
  657.  
  658.         // Filter duplicate JOIN clauses and combine into a single string.
  659.         if ( ! empty( $sql_chunks['join'] ) ) {
  660.             $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
  661.         }
  662.  
  663.         // Generate a single WHERE clause with proper brackets and indentation.
  664.         if ( ! empty( $sql_chunks['where'] ) ) {
  665.             $sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
  666.         }
  667.  
  668.         return $sql;
  669.     }
  670.  
  671.     /**
  672.      * Turns a single date clause into pieces for a WHERE clause.
  673.      *
  674.      * A wrapper for get_sql_for_clause(), included here for backward
  675.      * compatibility while retaining the naming convention across Query classes.
  676.      *
  677.      * @since  3.7.0
  678.      *
  679.      * @param  array $query Date query arguments.
  680.      * @return array {
  681.      *     Array containing JOIN and WHERE SQL clauses to append to the main query.
  682.      *
  683.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  684.      *     @type string $where SQL fragment to append to the main WHERE clause.
  685.      * }
  686.      */
  687.     protected function get_sql_for_subquery( $query ) {
  688.         return $this->get_sql_for_clause( $query, '' );
  689.     }
  690.  
  691.     /**
  692.      * Turns a first-order date query into SQL for a WHERE clause.
  693.      *
  694.      * @since  4.1.0
  695.      *
  696.      * @param  array $query        Date query clause.
  697.      * @param  array $parent_query Parent query of the current date query.
  698.      * @return array {
  699.      *     Array containing JOIN and WHERE SQL clauses to append to the main query.
  700.      *
  701.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  702.      *     @type string $where SQL fragment to append to the main WHERE clause.
  703.      * }
  704.      */
  705.     protected function get_sql_for_clause( $query, $parent_query ) {
  706.         global $wpdb;
  707.  
  708.         // The sub-parts of a $where part.
  709.         $where_parts = array();
  710.  
  711.         $column = ( ! empty( $query['column'] ) ) ? esc_sql( $query['column'] ) : $this->column;
  712.  
  713.         $column = $this->validate_column( $column );
  714.  
  715.         $compare = $this->get_compare( $query );
  716.  
  717.         $inclusive = ! empty( $query['inclusive'] );
  718.  
  719.         // Assign greater- and less-than values.
  720.         $lt = '<';
  721.         $gt = '>';
  722.  
  723.         if ( $inclusive ) {
  724.             $lt .= '=';
  725.             $gt .= '=';
  726.         }
  727.  
  728.         // Range queries.
  729.         if ( ! empty( $query['after'] ) ) {
  730.             $where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) );
  731.         }
  732.         if ( ! empty( $query['before'] ) ) {
  733.             $where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) );
  734.         }
  735.         // Specific value queries.
  736.  
  737.         if ( isset( $query['year'] ) && $value = $this->build_value( $compare, $query['year'] ) )
  738.             $where_parts[] = "YEAR( $column ) $compare $value";
  739.  
  740.         if ( isset( $query['month'] ) && $value = $this->build_value( $compare, $query['month'] ) ) {
  741.             $where_parts[] = "MONTH( $column ) $compare $value";
  742.         } elseif ( isset( $query['monthnum'] ) && $value = $this->build_value( $compare, $query['monthnum'] ) ) {
  743.             $where_parts[] = "MONTH( $column ) $compare $value";
  744.         }
  745.         if ( isset( $query['week'] ) && false !== ( $value = $this->build_value( $compare, $query['week'] ) ) ) {
  746.             $where_parts[] = _wp_mysql_week( $column ) . " $compare $value";
  747.         } elseif ( isset( $query['w'] ) && false !== ( $value = $this->build_value( $compare, $query['w'] ) ) ) {
  748.             $where_parts[] = _wp_mysql_week( $column ) . " $compare $value";
  749.         }
  750.         if ( isset( $query['dayofyear'] ) && $value = $this->build_value( $compare, $query['dayofyear'] ) )
  751.             $where_parts[] = "DAYOFYEAR( $column ) $compare $value";
  752.  
  753.         if ( isset( $query['day'] ) && $value = $this->build_value( $compare, $query['day'] ) )
  754.             $where_parts[] = "DAYOFMONTH( $column ) $compare $value";
  755.  
  756.         if ( isset( $query['dayofweek'] ) && $value = $this->build_value( $compare, $query['dayofweek'] ) )
  757.             $where_parts[] = "DAYOFWEEK( $column ) $compare $value";
  758.  
  759.         if ( isset( $query['dayofweek_iso'] ) && $value = $this->build_value( $compare, $query['dayofweek_iso'] ) )
  760.             $where_parts[] = "WEEKDAY( $column ) + 1 $compare $value";
  761.  
  762.         if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) {
  763.             // Avoid notices.
  764.             foreach ( array( 'hour', 'minute', 'second' ) as $unit ) {
  765.                 if ( ! isset( $query[ $unit ] ) ) {
  766.                     $query[ $unit ] = null;
  767.                 }
  768.             }
  769.  
  770.             if ( $time_query = $this->build_time_query( $column, $compare, $query['hour'], $query['minute'], $query['second'] ) ) {
  771.                 $where_parts[] = $time_query;
  772.             }
  773.         }
  774.  
  775.         /*
  776.          * Return an array of 'join' and 'where' for compatibility
  777.          * with other query classes.
  778.          */
  779.         return array(
  780.             'where' => $where_parts,
  781.             'join'  => array(),
  782.         );
  783.     }
  784.  
  785.     /**
  786.      * Builds and validates a value string based on the comparison operator.
  787.      *
  788.      * @since 3.7.0
  789.      *
  790.      * @param string $compare The compare operator to use
  791.      * @param string|array $value The value
  792.      * @return string|false|int The value to be used in SQL or false on error.
  793.      */
  794.     public function build_value( $compare, $value ) {
  795.         if ( ! isset( $value ) )
  796.             return false;
  797.  
  798.         switch ( $compare ) {
  799.             case 'IN':
  800.             case 'NOT IN':
  801.                 $value = (array) $value;
  802.  
  803.                 // Remove non-numeric values.
  804.                 $value = array_filter( $value, 'is_numeric' );
  805.  
  806.                 if ( empty( $value ) ) {
  807.                     return false;
  808.                 }
  809.  
  810.                 return '(' . implode( ',', array_map( 'intval', $value ) ) . ')';
  811.  
  812.             case 'BETWEEN':
  813.             case 'NOT BETWEEN':
  814.                 if ( ! is_array( $value ) || 2 != count( $value ) ) {
  815.                     $value = array( $value, $value );
  816.                 } else {
  817.                     $value = array_values( $value );
  818.                 }
  819.  
  820.                 // If either value is non-numeric, bail.
  821.                 foreach ( $value as $v ) {
  822.                     if ( ! is_numeric( $v ) ) {
  823.                         return false;
  824.                     }
  825.                 }
  826.  
  827.                 $value = array_map( 'intval', $value );
  828.  
  829.                 return $value[0] . ' AND ' . $value[1];
  830.  
  831.             default:
  832.                 if ( ! is_numeric( $value ) ) {
  833.                     return false;
  834.                 }
  835.  
  836.                 return (int) $value;
  837.         }
  838.     }
  839.  
  840.     /**
  841.      * Builds a MySQL format date/time based on some query parameters.
  842.      *
  843.      * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to
  844.      * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can
  845.      * pass a string that will be run through strtotime().
  846.      *
  847.      * @since 3.7.0
  848.      *
  849.      * @param string|array $datetime       An array of parameters or a strotime() string
  850.      * @param bool         $default_to_max Whether to round up incomplete dates. Supported by values
  851.      *                                     of $datetime that are arrays, or string values that are a
  852.      *                                     subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i').
  853.      *                                     Default: false.
  854.      * @return string|false A MySQL format date/time or false on failure
  855.      */
  856.     public function build_mysql_datetime( $datetime, $default_to_max = false ) {
  857.         $now = current_time( 'timestamp' );
  858.  
  859.         if ( ! is_array( $datetime ) ) {
  860.  
  861.             /*
  862.              * Try to parse some common date formats, so we can detect
  863.              * the level of precision and support the 'inclusive' parameter.
  864.              */
  865.             if ( preg_match( '/^(\d{4})$/', $datetime, $matches ) ) {
  866.                 // Y
  867.                 $datetime = array(
  868.                     'year' => intval( $matches[1] ),
  869.                 );
  870.  
  871.             } elseif ( preg_match( '/^(\d{4})\-(\d{2})$/', $datetime, $matches ) ) {
  872.                 // Y-m
  873.                 $datetime = array(
  874.                     'year'  => intval( $matches[1] ),
  875.                     'month' => intval( $matches[2] ),
  876.                 );
  877.  
  878.             } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2})$/', $datetime, $matches ) ) {
  879.                 // Y-m-d
  880.                 $datetime = array(
  881.                     'year'  => intval( $matches[1] ),
  882.                     'month' => intval( $matches[2] ),
  883.                     'day'   => intval( $matches[3] ),
  884.                 );
  885.  
  886.             } elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2}) (\d{2}):(\d{2})$/', $datetime, $matches ) ) {
  887.                 // Y-m-d H:i
  888.                 $datetime = array(
  889.                     'year'   => intval( $matches[1] ),
  890.                     'month'  => intval( $matches[2] ),
  891.                     'day'    => intval( $matches[3] ),
  892.                     'hour'   => intval( $matches[4] ),
  893.                     'minute' => intval( $matches[5] ),
  894.                 );
  895.             }
  896.  
  897.             // If no match is found, we don't support default_to_max.
  898.             if ( ! is_array( $datetime ) ) {
  899.                 // @todo Timezone issues here possibly
  900.                 return gmdate( 'Y-m-d H:i:s', strtotime( $datetime, $now ) );
  901.             }
  902.         }
  903.  
  904.         $datetime = array_map( 'absint', $datetime );
  905.  
  906.         if ( ! isset( $datetime['year'] ) )
  907.             $datetime['year'] = gmdate( 'Y', $now );
  908.  
  909.         if ( ! isset( $datetime['month'] ) )
  910.             $datetime['month'] = ( $default_to_max ) ? 12 : 1;
  911.  
  912.         if ( ! isset( $datetime['day'] ) )
  913.             $datetime['day'] = ( $default_to_max ) ? (int) date( 't', mktime( 0, 0, 0, $datetime['month'], 1, $datetime['year'] ) ) : 1;
  914.  
  915.         if ( ! isset( $datetime['hour'] ) )
  916.             $datetime['hour'] = ( $default_to_max ) ? 23 : 0;
  917.  
  918.         if ( ! isset( $datetime['minute'] ) )
  919.             $datetime['minute'] = ( $default_to_max ) ? 59 : 0;
  920.  
  921.         if ( ! isset( $datetime['second'] ) )
  922.             $datetime['second'] = ( $default_to_max ) ? 59 : 0;
  923.  
  924.         return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $datetime['year'], $datetime['month'], $datetime['day'], $datetime['hour'], $datetime['minute'], $datetime['second'] );
  925.     }
  926.  
  927.     /**
  928.      * Builds a query string for comparing time values (hour, minute, second).
  929.      *
  930.      * If just hour, minute, or second is set than a normal comparison will be done.
  931.      * However if multiple values are passed, a pseudo-decimal time will be created
  932.      * in order to be able to accurately compare against.
  933.      *
  934.      * @since 3.7.0
  935.      *
  936.      * @param string $column The column to query against. Needs to be pre-validated!
  937.      * @param string $compare The comparison operator. Needs to be pre-validated!
  938.      * @param int|null $hour Optional. An hour value (0-23).
  939.      * @param int|null $minute Optional. A minute value (0-59).
  940.      * @param int|null $second Optional. A second value (0-59).
  941.      * @return string|false A query part or false on failure.
  942.      */
  943.     public function build_time_query( $column, $compare, $hour = null, $minute = null, $second = null ) {
  944.         global $wpdb;
  945.  
  946.         // Have to have at least one
  947.         if ( ! isset( $hour ) && ! isset( $minute ) && ! isset( $second ) )
  948.             return false;
  949.  
  950.         // Complex combined queries aren't supported for multi-value queries
  951.         if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
  952.             $return = array();
  953.  
  954.             if ( isset( $hour ) && false !== ( $value = $this->build_value( $compare, $hour ) ) )
  955.                 $return[] = "HOUR( $column ) $compare $value";
  956.  
  957.             if ( isset( $minute ) && false !== ( $value = $this->build_value( $compare, $minute ) ) )
  958.                 $return[] = "MINUTE( $column ) $compare $value";
  959.  
  960.             if ( isset( $second ) && false !== ( $value = $this->build_value( $compare, $second ) ) )
  961.                 $return[] = "SECOND( $column ) $compare $value";
  962.  
  963.             return implode( ' AND ', $return );
  964.         }
  965.  
  966.         // Cases where just one unit is set
  967.         if ( isset( $hour ) && ! isset( $minute ) && ! isset( $second ) && false !== ( $value = $this->build_value( $compare, $hour ) ) ) {
  968.             return "HOUR( $column ) $compare $value";
  969.         } elseif ( ! isset( $hour ) && isset( $minute ) && ! isset( $second ) && false !== ( $value = $this->build_value( $compare, $minute ) ) ) {
  970.             return "MINUTE( $column ) $compare $value";
  971.         } elseif ( ! isset( $hour ) && ! isset( $minute ) && isset( $second ) && false !== ( $value = $this->build_value( $compare, $second ) ) ) {
  972.             return "SECOND( $column ) $compare $value";
  973.         }
  974.  
  975.         // Single units were already handled. Since hour & second isn't allowed, minute must to be set.
  976.         if ( ! isset( $minute ) )
  977.             return false;
  978.  
  979.         $format = $time = '';
  980.  
  981.         // Hour
  982.         if ( null !== $hour ) {
  983.             $format .= '%H.';
  984.             $time   .= sprintf( '%02d', $hour ) . '.';
  985.         } else {
  986.             $format .= '0.';
  987.             $time   .= '0.';
  988.         }
  989.  
  990.         // Minute
  991.         $format .= '%i';
  992.         $time   .= sprintf( '%02d', $minute );
  993.  
  994.         if ( isset( $second ) ) {
  995.             $format .= '%s';
  996.             $time   .= sprintf( '%02d', $second );
  997.         }
  998.  
  999.         return $wpdb->prepare( "DATE_FORMAT( $column, %s ) $compare %f", $format, $time );
  1000.     }
  1001. }
  1002.