parse_args( $args ); // Maybe set variables from arguments if ( ! empty( $r ) ) { $this->set_vars( $r ); } } /** Argument Handlers *****************************************************/ /** * Parse column arguments * * @since 1.0.0 * @param array $args Default empty array. * @return array */ private function parse_args( $args = array() ) { // Parse arguments $r = wp_parse_args( $args, array( // Table 'name' => '', 'type' => '', 'length' => '', 'unsigned' => false, 'zerofill' => false, 'binary' => false, 'allow_null' => false, 'default' => '', 'extra' => '', 'encoding' => $this->get_db()->charset, 'collation' => $this->get_db()->collate, 'comment' => '', // Query 'pattern' => false, 'searchable' => false, 'sortable' => false, 'date_query' => false, 'transition' => false, 'in' => true, 'not_in' => true, // Special 'primary' => false, 'created' => false, 'modified' => false, 'uuid' => false, // Cache 'cache_key' => false, // Validation 'validate' => '', // Capabilities 'caps' => array(), // Backwards Compatibility 'aliases' => array(), // Column Relationships 'relationships' => array() ) ); // Force some arguments for special column types $r = $this->special_args( $r ); // Set the args before they are sanitized $this->set_vars( $r ); // Return array return $this->validate_args( $r ); } /** * Validate arguments after they are parsed. * * @since 1.0.0 * @param array $args Default empty array. * @return array */ private function validate_args( $args = array() ) { // Sanitization callbacks $callbacks = array( 'name' => 'sanitize_key', 'type' => 'strtoupper', 'length' => 'intval', 'unsigned' => 'wp_validate_boolean', 'zerofill' => 'wp_validate_boolean', 'binary' => 'wp_validate_boolean', 'allow_null' => 'wp_validate_boolean', 'default' => array( $this, 'sanitize_default' ), 'extra' => 'wp_kses_data', 'encoding' => 'wp_kses_data', 'collation' => 'wp_kses_data', 'comment' => 'wp_kses_data', 'primary' => 'wp_validate_boolean', 'created' => 'wp_validate_boolean', 'modified' => 'wp_validate_boolean', 'uuid' => 'wp_validate_boolean', 'searchable' => 'wp_validate_boolean', 'sortable' => 'wp_validate_boolean', 'date_query' => 'wp_validate_boolean', 'transition' => 'wp_validate_boolean', 'in' => 'wp_validate_boolean', 'not_in' => 'wp_validate_boolean', 'cache_key' => 'wp_validate_boolean', 'pattern' => array( $this, 'sanitize_pattern' ), 'validate' => array( $this, 'sanitize_validation' ), 'caps' => array( $this, 'sanitize_capabilities' ), 'aliases' => array( $this, 'sanitize_aliases' ), 'relationships' => array( $this, 'sanitize_relationships' ) ); // Default args array $r = array(); // Loop through and try to execute callbacks foreach ( $args as $key => $value ) { // Callback is callable if ( isset( $callbacks[ $key ] ) && is_callable( $callbacks[ $key ] ) ) { $r[ $key ] = call_user_func( $callbacks[ $key ], $value ); // Callback is malformed so just let it through to avoid breakage } else { $r[ $key ] = $value; } } // Return sanitized arguments return $r; } /** * Force column arguments for special column types * * @since 1.0.0 * @param array $args Default empty array. * @return array */ private function special_args( $args = array() ) { // Primary key columns are always used as cache keys if ( ! empty( $args['primary'] ) ) { $args['cache_key'] = true; // All UUID columns need to follow a very specific pattern } elseif ( ! empty( $args['uuid'] ) ) { $args['name'] = 'uuid'; $args['type'] = 'varchar'; $args['length'] = '100'; $args['in'] = false; $args['not_in'] = false; $args['searchable'] = false; $args['sortable'] = false; } // Return args return (array) $args; } /** Public Helpers ********************************************************/ /** * Return if a column type is numeric or not. * * @since 1.0.0 * @return bool */ public function is_numeric() { return $this->is_type( array( 'tinyint', 'int', 'mediumint', 'bigint' ) ); } /** Private Helpers *******************************************************/ /** * Return if this column is of a certain type. * * @since 1.0.0 * @param mixed $type Default empty string. The type to check. Also accepts an array. * @return bool True if of type, False if not */ private function is_type( $type = '' ) { // If string, cast to array if ( is_string( $type ) ) { $type = (array) $type; } // Make them lowercase $types = array_map( 'strtolower', $type ); // Return if match or not return (bool) in_array( strtolower( $this->type ), $types, true ); } /** Private Sanitizers ****************************************************/ /** * Sanitize capabilities array * * @since 1.0.0 * @param array $caps Default empty array. * @return array */ private function sanitize_capabilities( $caps = array() ) { return wp_parse_args( $caps, array( 'select' => 'exist', 'insert' => 'exist', 'update' => 'exist', 'delete' => 'exist' ) ); } /** * Sanitize aliases array using `sanitize_key()` * * @since 1.0.0 * @param array $aliases Default empty array. * @return array */ private function sanitize_aliases( $aliases = array() ) { return array_map( 'sanitize_key', $aliases ); } /** * Sanitize relationships array * * @todo * @since 1.0.0 * @param array $relationships Default empty array. * @return array */ private function sanitize_relationships( $relationships = array() ) { return array_filter( $relationships ); } /** * Sanitize the default value * * @since 1.0.0 * @param string $default * @return string|null */ private function sanitize_default( $default = '' ) { // Null if ( ( true === $this->allow_null ) && is_null( $default ) ) { return null; // String } elseif ( is_string( $default ) ) { return wp_kses_data( $default ); // Integer } elseif ( $this->is_numeric( $default ) ) { return (int) $default; } // @todo datetime, decimal, and other column types // Unknown, so return the default's default return ''; } /** * Sanitize the pattern * * @since 1.0.0 * @param mixed $pattern * @return string */ private function sanitize_pattern( $pattern = false ) { // Allowed patterns $allowed_patterns = array( '%s', '%d', '%f' ); // Return pattern if allowed if ( in_array( $pattern, $allowed_patterns, true ) ) { return $pattern; } // Fallback to digit or string return $this->is_numeric() ? '%d' : '%s'; } /** * Sanitize the validation callback * * @since 1.0.0 * @param string $callback Default empty string. A callable PHP function name or method * @return string The most appropriate callback function for the value */ private function sanitize_validation( $callback = '' ) { // Return callback if it's callable if ( is_callable( $callback ) ) { return $callback; } // UUID special column if ( true === $this->uuid ) { $callback = array( $this, 'validate_uuid' ); // Datetime fallback } elseif ( $this->is_type( 'datetime' ) ) { $callback = array( $this, 'validate_datetime' ); // Decimal fallback } elseif ( $this->is_type( 'decimal' ) ) { $callback = array( $this, 'validate_decimal' ); // Intval fallback } elseif ( $this->is_numeric() ) { $callback = 'intval'; } // Return the callback return $callback; } /** Public Validators *****************************************************/ /** * Fallback to validate a datetime value if no other is set. * * This assumes NO_ZERO_DATES is off or overridden. * * If MySQL drops support for zero dates, this method will need to be * updated to support different default values based on the environment. * * @since 1.0.0 * @param string $value Default ''. A datetime value that needs validating * @return string A valid datetime value */ public function validate_datetime( $value = '' ) { // Handle "empty" values if ( empty( $value ) || ( '0000-00-00 00:00:00' === $value ) ) { $value = ! empty( $this->default ) ? $this->default : ''; // Convert to MySQL datetime format via date() && strtotime } elseif ( function_exists( 'date' ) ) { $value = date( 'Y-m-d H:i:s', strtotime( $value ) ); } // Return the validated value return $value; } /** * Validate a decimal * * (Recommended decimal column length is '18,9'.) * * This is used to validate a mixed value before it is saved into a decimal * column in a database table. * * Uses number_format() which does rounding to the last decimal if your * value is longer than specified. * * @since 1.0.0 * @param mixed $value Default empty string. The decimal value to validate * @param int $decimals Default 9. The number of decimal points to accept * @return float */ public function validate_decimal( $value = 0, $decimals = 9 ) { // Protect against non-numeric values if ( ! is_numeric( $value ) ) { $value = 0; } // Protect against non-numeric decimals if ( ! is_numeric( $decimals ) ) { $decimals = 9; } // Is the value negative? $negative_exponent = ( $value < 0 ) ? -1 : 1; // Only numbers and period $value = preg_replace( '/[^0-9\.]/', '', (string) $value ); // Format to number of decimals, and cast as float $formatted = number_format( $value, $decimals, '.', '' ); // Adjust for negative values $retval = $formatted * $negative_exponent; // Return return $retval; } /** * Validate a UUID. * * This uses the v4 algorithm to generate a UUID that is used to uniquely * and universally identify a given database row without any direct * connection or correlation to the data in that row. * * From http://php.net/manual/en/function.uniqid.php#94959 * * @since 1.0.0 * @param string $value The UUID value (empty on insert, string on update) * @return string Generated UUID. */ public function validate_uuid( $value = '' ) { // Default URN UUID prefix $prefix = 'urn:uuid:'; // Bail if not empty and correctly prefixed // (UUIDs should _never_ change once they are set) if ( ! empty( $value ) && ( 0 === strpos( $value, $prefix ) ) ) { return $value; } // Put the pieces together $value = sprintf( "{$prefix}%04x%04x-%04x-%04x-%04x-%04x%04x%04x", // 32 bits for "time_low" mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), // 16 bits for "time_mid" mt_rand( 0, 0xffff ), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 4 mt_rand( 0, 0x0fff ) | 0x4000, // 16 bits, 8 bits for "clk_seq_hi_res", // 8 bits for "clk_seq_low", // two most significant bits holds zero and one for variant DCE1.1 mt_rand( 0, 0x3fff ) | 0x8000, // 48 bits for "node" mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) ); // Return the new UUID return $value; } /** Table Helpers *********************************************************/ /** * Return a string representation of what this column's properties look like * in a MySQL. * * @todo * @since 1.0.0 * @return string */ public function get_create_string() { // Default return val $retval = ''; // Bail if no name if ( ! empty( $this->name ) ) { $retval .= $this->name; } // Type if ( ! empty( $this->type ) ) { $retval .= " {$this->type}"; } // Length if ( ! empty( $this->length ) ) { $retval .= '(' . $this->length . ')'; } // Unsigned if ( ! empty( $this->unsigned ) ) { $retval .= " unsigned"; } // Zerofill if ( ! empty( $this->zerofill ) ) { // TBD } // Binary if ( ! empty( $this->binary ) ) { // TBD } // Allow null if ( ! empty( $this->allow_null ) ) { $retval .= " NOT NULL "; } // Default if ( ! empty( $this->default ) ) { $retval .= " default '{$this->default}'"; // A literal false means no default value } elseif ( false !== $this->default ) { // Numeric if ( $this->is_numeric() ) { $retval .= " default '0'"; } elseif ( $this->is_type( 'datetime' ) ) { $retval .= " default '0000-00-00 00:00:00'"; } else { $retval .= " default ''"; } } // Extra if ( ! empty( $this->extra ) ) { $retval .= " {$this->extra}"; } // Encoding if ( ! empty( $this->encoding ) ) { } else { } // Collation if ( ! empty( $this->collation ) ) { } else { } // Return the create string return $retval; } }