├── CONTRIBUTING.md ├── claws.php └── phpunit.xml.dist /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing To Claws 2 | 3 | Community made patches, localizations, bug reports and contributions are always welcome and are crucial to ensure Claws' success in the WordPress ecosystem. 4 | 5 | When contributing please ensure you follow the guidelines below so that we can keep on top of things. 6 | 7 | ## Getting Started 8 | 9 | * __Do not report potential security vulnerabilities here. Contact our security team privately via [sandhillsdev.com/contact/](http://sandhillsdev.com/contact/).__ 10 | * Submit a ticket for your issue, assuming one does not already exist. 11 | * Raise it on our [Issue Tracker](https://github.com/sandhillsdevelopment/claws/issues) 12 | * Clearly describe the issue including steps to reproduce the bug. 13 | * Make sure you fill in the earliest version that you know has the issue as well as the version of WordPress you're using. 14 | 15 | ## Making Changes 16 | 17 | * Fork the repository on GitHub 18 | * Make the changes to your forked repository 19 | * Ensure you stick to the [WordPress Coding Standards](https://codex.wordpress.org/WordPress_Coding_Standards) 20 | * When committing, reference your issue (if present) and include a note about the fix 21 | * If possible, and if applicable, please also add/update unit tests for your changes 22 | * Push the changes to your fork and submit a pull request to the `master` branch of the claws repository 23 | 24 | ## Code Documentation 25 | 26 | * We ensure that every Claws method is documented well and follows the [WordPress Inline Documentation Standards for PHP](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/php/) 27 | * Please make sure that every function is documented so that when we update our API Documentation things don't go awry! 28 | * Finally, please use tabs and not spaces. The tab indent size should be 4 for all Claws code. 29 | 30 | At this point you're waiting on us to merge your pull request. We'll review all pull requests, and make suggestions and changes if necessary. 31 | 32 | # Additional Resources 33 | * [General GitHub Documentation](https://help.github.com/) 34 | * [GitHub Pull Request documentation](https://help.github.com/send-pull-requests/) 35 | * [PHPUnit Tests Guide](https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html) 36 | -------------------------------------------------------------------------------- /claws.php: -------------------------------------------------------------------------------- 1 | version; 104 | } 105 | 106 | /** 107 | * Handles calling pseudo-methods. 108 | * 109 | * @since 1.0.0 110 | * 111 | * @param string $name Method name. 112 | * @param array $args Method arguments. 113 | */ 114 | public function __call( $name, $args ) { 115 | 116 | /* 117 | * Prior to PHP 7, reserved keywords could not be used in method names, 118 | * so having or()/and() methods wouldn't be allowed. Using __call() allows 119 | * us to circumvent that problem. 120 | */ 121 | switch( $name ) { 122 | 123 | case 'or': 124 | $clause = isset( $args[0] ) ? $args[0] : null; 125 | 126 | // Shared logic. 127 | $this->__set_current_operator( 'OR', $clause ); 128 | 129 | return $this; 130 | break; 131 | 132 | case 'and': 133 | $clause = isset( $args[0] ) ? $args[0] : null; 134 | 135 | // Shared logic. 136 | $this->__set_current_operator( 'AND', $clause ); 137 | 138 | return $this; 139 | break; 140 | } 141 | 142 | } 143 | 144 | /** 145 | * Builds a section of the WHERE clause. 146 | * 147 | * @since 1.0.0 148 | * 149 | * @param mixed $values Single value of varying types, or array of values. 150 | * @param string|callable $callback_or_type Sanitization callback to pass values through, or shorthand 151 | * types to use preset callbacks. Default 'esc_sql'. 152 | * @param string $compare_type MySQL operator used for comparing the $value. Accepts '=', '!=', 153 | * '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 154 | * 'NOT BETWEEN', 'EXISTS' or 'NOT EXISTS'. 155 | * Default is 'IN' when `$value` is an array, '=' otherwise. 156 | * @return \Sandhills\Claws Current Claws instance. 157 | */ 158 | public function where( $field, $compare_type = null, $values = null, $callback_or_type = 'esc_sql' ) { 159 | $this->set_current_clause( 'where' ); 160 | $this->set_current_field( $field ); 161 | 162 | // Handle shorthand comparison phrases. 163 | if ( isset( $compare_type ) && isset( $values ) ) { 164 | 165 | $callback = $this->get_callback( $callback_or_type ); 166 | 167 | $this->compare( $compare_type, $values, $callback ); 168 | } 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Handles delegating short-hand value comparison phrases. 175 | * 176 | * @since 1.0.0 177 | * 178 | * @param string $type Type of comparison. Accepts '=', '!=', '<', '>', '>=', or '<='. 179 | * @param string|int|array $values Single value(s) of varying type, or an array of values. 180 | * @param callable $callback Callback to pass to the comparison method. 181 | * @return \Sandhills\Claws Current Claws instance. 182 | */ 183 | public function compare( $type, $values, $callback ) { 184 | switch( $type ) { 185 | case '!=': 186 | $this->doesnt_equal( $values, $callback ); 187 | break; 188 | 189 | case '<': 190 | $this->lt( $values, $callback ); 191 | break; 192 | 193 | case '>': 194 | $this->gt( $values, $callback ); 195 | break; 196 | 197 | case '<=': 198 | $this->lte( $values, $callback ); 199 | break; 200 | 201 | case '>=': 202 | $this->gte( $values, $callback ); 203 | break; 204 | 205 | case '=': 206 | default: 207 | $this->equals( $values, $callback ); 208 | break; 209 | } 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * Handles '=' value comparison. 216 | * 217 | * @since 1.0.0 218 | * 219 | * @param mixed $values Value of varying types, or array of values. 220 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 221 | * types to use preset callbacks. Default 'esc_sql'. 222 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 223 | * building the expression. Default 'OR'. 224 | * 225 | * @return \Sandhills\Claws Current Claws instance. 226 | */ 227 | public function equals( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 228 | $sql = $this->get_comparison_sql( $values, $callback_or_type, '=', $operator ); 229 | 230 | $this->add_clause_sql( $sql ); 231 | 232 | return $this; 233 | } 234 | 235 | /** 236 | * Handles '!=' value comparison. 237 | * 238 | * @since 1.0.0 239 | * 240 | * @param mixed $values Value of varying types, or array of values. 241 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 242 | * types to use preset callbacks. Default 'esc_sql'. 243 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 244 | * building the expression. Default 'OR'. 245 | * 246 | * @return \Sandhills\Claws Current Claws instance. 247 | */ 248 | public function doesnt_equal( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 249 | $sql = $this->get_comparison_sql( $values, $callback_or_type, '!=', $operator ); 250 | 251 | $this->add_clause_sql( $sql ); 252 | 253 | return $this; 254 | } 255 | 256 | /** 257 | * Handles '>' value comparison. 258 | * 259 | * @since 1.0.0 260 | * 261 | * @param mixed $values Value of varying types, or array of values. 262 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 263 | * types to use preset callbacks. Default 'esc_sql'. 264 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 265 | * building the expression. Default 'OR'. 266 | * 267 | * @return \Sandhills\Claws Current Claws instance. 268 | */ 269 | public function gt( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 270 | $sql = $this->get_comparison_sql( $values, $callback_or_type, '>', $operator ); 271 | 272 | $this->add_clause_sql( $sql ); 273 | 274 | return $this; 275 | } 276 | 277 | /** 278 | * Handles '<' value comparison. 279 | * 280 | * @since 1.0.0 281 | * 282 | * @param mixed $values Value of varying types, or array of values. 283 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 284 | * types to use preset callbacks. Default 'esc_sql'. 285 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 286 | * building the expression. Default 'OR'. 287 | * 288 | * @return \Sandhills\Claws Current Claws instance. 289 | */ 290 | public function lt( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 291 | $sql = $this->get_comparison_sql( $values, $callback_or_type, '<', $operator ); 292 | 293 | $this->add_clause_sql( $sql ); 294 | 295 | return $this; 296 | } 297 | 298 | /** 299 | * Handles '>=' value comparison. 300 | * 301 | * @since 1.0.0 302 | * 303 | * @param mixed $values Value of varying types, or array of values. 304 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 305 | * types to use preset callbacks. Default 'esc_sql'. 306 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 307 | * building the expression. Default 'OR'. 308 | * 309 | * @return \Sandhills\Claws Current Claws instance. 310 | */ 311 | public function gte( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 312 | $sql = $this->get_comparison_sql( $values, $callback_or_type, '>=', $operator ); 313 | 314 | $this->add_clause_sql( $sql ); 315 | 316 | return $this; 317 | } 318 | 319 | /** 320 | * Handles '<=' value comparison. 321 | * 322 | * @since 1.0.0 323 | * 324 | * @param mixed $values Value of varying types, or array of values. 325 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 326 | * types to use preset callbacks. Default 'esc_sql'. 327 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 328 | * building the expression. Default 'OR'. 329 | * 330 | * @return \Sandhills\Claws Current Claws instance. 331 | */ 332 | public function lte( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 333 | $sql = $this->get_comparison_sql( $values, $callback_or_type, '<=', $operator ); 334 | 335 | $this->add_clause_sql( $sql ); 336 | 337 | return $this; 338 | } 339 | 340 | /** 341 | * Handles 'LIKE' value comparison. 342 | * 343 | * @since 1.0.0 344 | * 345 | * @param mixed $values Value of varying types, or array of values. 346 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 347 | * types to use preset callbacks. Default `Claws->esc_like()`. 348 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 349 | * building the expression. Default 'OR'. 350 | * 351 | * @return \Sandhills\Claws Current Claws instance. 352 | */ 353 | public function like( $values, $callback_or_type = 'esc_like', $operator = 'OR' ) { 354 | $sql = $this->get_like_sql( $values, $callback_or_type, 'LIKE', $operator ); 355 | 356 | $this->add_clause_sql( $sql ); 357 | 358 | return $this; 359 | } 360 | 361 | /** 362 | * Handles 'NOT LIKE' value comparison. 363 | * 364 | * @since 1.0.0 365 | * 366 | * @param mixed $values Value of varying types, or array of values. 367 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 368 | * types to use preset callbacks. Default is `Claws->esc_like()`. 369 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 370 | * building the expression. Default 'OR'. 371 | * 372 | * @return \Sandhills\Claws Current Claws instance. 373 | */ 374 | public function not_like( $values, $callback_or_type = 'esc_like', $operator = 'OR' ) { 375 | $sql = $this->get_like_sql( $values, $callback_or_type, 'NOT LIKE', $operator ); 376 | 377 | $this->add_clause_sql( $sql ); 378 | 379 | return $this; 380 | } 381 | 382 | /** 383 | * Handles 'IN' value comparison. 384 | * 385 | * @since 1.0.0 386 | * 387 | * @param mixed $values Value of varying types, or array of values. 388 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 389 | * types to use preset callbacks. Default 'esc_sql'. 390 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 391 | * building the expression. Default 'OR'. 392 | * 393 | * @return \Sandhills\Claws Current Claws instance. 394 | */ 395 | public function in( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 396 | if ( ! is_array( $values ) ) { 397 | 398 | $this->equals( $values, $callback_or_type, $operator ); 399 | 400 | } else { 401 | 402 | $sql = $this->get_in_sql( $values, $callback_or_type, 'IN' ); 403 | 404 | $this->add_clause_sql( $sql ); 405 | } 406 | 407 | return $this; 408 | } 409 | 410 | /** 411 | * Handles 'NOT IN' value comparison. 412 | * 413 | * @since 1.0.0 414 | * 415 | * @param mixed $values Value of varying types, or array of values. 416 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 417 | * types to use preset callbacks. Default 'esc_sql'. 418 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 419 | * building the expression. Default 'OR'. 420 | * 421 | * @return \Sandhills\Claws Current Claws instance. 422 | */ 423 | public function not_in( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 424 | if ( ! is_array( $values ) ) { 425 | 426 | $this->doesnt_equal( $values, $callback_or_type, $operator ); 427 | 428 | } else { 429 | 430 | $sql = $this->get_in_sql( $values, $callback_or_type, 'NOT IN' ); 431 | 432 | $this->add_clause_sql( $sql ); 433 | } 434 | 435 | return $this; 436 | } 437 | 438 | /** 439 | * Handles 'BETWEEN' value comparison. 440 | * 441 | * Note: If doing a between comparison for dates, care should be taken to ensure 442 | * the beginning and ending dates represent the beginning and/or end of the day 443 | * including hours, minutes, and seconds, depending on the expected range. 444 | * 445 | * @since 1.0.0 446 | * 447 | * @param array $values Array of values. 448 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 449 | * types to use preset callbacks. Default 'esc_sql'. 450 | * 451 | * @return \Sandhills\Claws Current Claws instance. 452 | */ 453 | public function between( $values, $callback_or_type = 'esc_sql' ) { 454 | $sql = $this->get_between_sql( $values, $callback_or_type, 'BETWEEN' ); 455 | 456 | $this->add_clause_sql( $sql ); 457 | 458 | return $this; 459 | } 460 | 461 | /** 462 | * Handles 'NOT BETWEEN' value comparison. 463 | * 464 | * @since 1.0.0 465 | * 466 | * @param mixed $values Value of varying types, or array of values. 467 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 468 | * types to use preset callbacks. Default 'esc_sql'. 469 | * 470 | * @return \Sandhills\Claws Current Claws instance. 471 | */ 472 | public function not_between( $values, $callback_or_type = 'esc_sql' ) { 473 | $sql = $this->get_between_sql( $values, $callback_or_type, 'NOT BETWEEN' ); 474 | 475 | $this->add_clause_sql( $sql ); 476 | 477 | return $this; 478 | } 479 | 480 | /** 481 | * Handles 'EXISTS' value comparison. 482 | * 483 | * @since 1.0.0 484 | * 485 | * @param mixed $values Value of varying types, or array of values. 486 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 487 | * types to use preset callbacks. Default 'esc_sql'. 488 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 489 | * building the expression. Default 'OR'. 490 | * 491 | * @return \Sandhills\Claws Current Claws instance. 492 | */ 493 | public function exists( $values, $callback_or_type = 'esc_sql', $operator = 'OR' ) { 494 | return $this->equals( $values, $callback_or_type, $operator ); 495 | } 496 | 497 | /** 498 | * Handles 'NOT EXISTS' value comparison. 499 | * 500 | * @since 1.0.0 501 | * 502 | * @param string|callable $callback_or_type Optional. Sanitization callback to pass values through, or shorthand 503 | * types to use preset callbacks. Default 'esc_sql'. 504 | * @param string $operator Optional. If `$value` is an array, whether to use 'OR' or 'AND' when 505 | * building the expression. Default 'OR'. 506 | * 507 | * @return \Sandhills\Claws Current Claws instance. 508 | */ 509 | public function not_exists( $callback_or_type = 'esc_sql', $operator = 'OR' ) { 510 | $sql = $this->build_comparison_sql( array( '' ), 'IS NULL', $operator ); 511 | 512 | $this->add_clause_sql( $sql ); 513 | 514 | return $this; 515 | } 516 | 517 | /** 518 | * Helper used by direct comparison methods to build SQL. 519 | * 520 | * @since 1.0.0 521 | * 522 | * @param array $values Array of values to compare. 523 | * @param string|callable $callback_or_type Sanitization callback to pass values through, or shorthand 524 | * types to use preset callbacks. 525 | * @param string $compare_type Comparison type to make. Accepts '=', '!=', '<', '>', '<=', or '>='. 526 | * Default '='. 527 | * @param string $operator Optional. Operator to use between multiple sets of value comparisons. 528 | * Accepts 'OR' or 'AND'. Default 'OR'. 529 | * @return string Raw, sanitized SQL. 530 | */ 531 | protected function get_comparison_sql( $values, $callback_or_type, $compare_type, $operator = 'OR' ) { 532 | if ( ! in_array( $compare_type, array( '=', '!=', '<', '>', '<=', '>=' ) ) ) { 533 | $compare_type = '='; 534 | } 535 | 536 | $callback = $this->get_callback( $callback_or_type ); 537 | $operator = $this->get_operator( $operator ); 538 | $values = $this->prepare_values( $values ); 539 | 540 | // Sanitize the values and built the SQL. 541 | $values = array_map( $callback, $values ); 542 | 543 | return $this->build_comparison_sql( $values, $compare_type, $operator ); 544 | } 545 | 546 | /** 547 | * Builds and retrieves the actual comparison SQL. 548 | * 549 | * @since 1.0.0 550 | * 551 | * @param array $values Array of values. 552 | * @param string $compare_type Comparison type to make. Accepts '=', '!=', '<', '>', '<=', or '>='. 553 | * Default '='. 554 | * @param string $operator Operator to use between value comparisons. 555 | * @return string Comparison SQL. 556 | */ 557 | protected function build_comparison_sql( $values, $compare_type, $operator ) { 558 | global $wpdb; 559 | 560 | $sql = ''; 561 | 562 | $count = count( $values ); 563 | $current = 0; 564 | $field = $this->get_current_field(); 565 | 566 | // Loop through the values and bring in $operator if needed. 567 | foreach ( $values as $value ) { 568 | $type = $this->get_cast_for_type( gettype( $value ) ); 569 | 570 | $value = $wpdb->prepare( '%s', $value ); 571 | 572 | if ( 'CHAR' !== $type ) { 573 | $value = "CAST( {$value} AS {$type} )"; 574 | } 575 | 576 | $sql .= "`{$field}` {$compare_type} {$value}"; 577 | 578 | if ( ++$current !== $count ) { 579 | $sql .= " {$operator} "; 580 | } 581 | } 582 | 583 | // Finish the phrase. 584 | if ( $count > 1 ) { 585 | $sql = '( ' . $sql . ' )'; 586 | } 587 | 588 | return $sql; 589 | } 590 | 591 | /** 592 | * Helper used by 'in' comparison methods to build SQL. 593 | * 594 | * @since 1.0.0 595 | * 596 | * @param array $values Array of values to compare. 597 | * @param string|callable $callback_or_type Sanitization callback to pass values through, or shorthand 598 | * types to use preset callbacks. 599 | * @param string $compare_type Comparison to make. Accepts 'IN' or 'NOT IN'. 600 | * @return string Raw, sanitized SQL. 601 | */ 602 | protected function get_in_sql( $values, $callback_or_type, $compare_type ) { 603 | $field = $this->get_current_field(); 604 | $callback = $this->get_callback( $callback_or_type ); 605 | $compare_type = strtoupper( $compare_type ); 606 | 607 | if ( ! in_array( $compare_type, array( 'IN', 'NOT IN' ) ) ) { 608 | $compare_type = 'IN'; 609 | } 610 | 611 | // Escape values. 612 | $values = array_map( function( $value ) use ( $callback ) { 613 | $value = call_user_func( $callback, $value ); 614 | 615 | if ( 'string' === gettype( $value ) ) { 616 | $value = "'{$value}'"; 617 | } 618 | 619 | return $value; 620 | }, $values ); 621 | 622 | $values = implode( ', ', $values ); 623 | 624 | $sql = "{$field} {$compare_type}( {$values} )"; 625 | 626 | return $sql; 627 | } 628 | 629 | /** 630 | * Helper used by 'LIKE' comparison methods to build SQL. 631 | * 632 | * @since 1.0.0 633 | * 634 | * @param array $values Array of values to compare. 635 | * @param string|callable $callback_or_type Sanitization callback to pass values through, or shorthand 636 | * types to use preset callbacks. 637 | * @param string $compare_type Comparison to make. Accepts 'LIKE' or 'NOT LIKE'. 638 | * @return string Raw, sanitized SQL. 639 | */ 640 | protected function get_like_sql( $values, $callback_or_type, $compare_type, $operator ) { 641 | $sql = ''; 642 | 643 | $callback = $this->get_callback( $callback_or_type ); 644 | $field = $this->get_current_field(); 645 | $values = $this->prepare_values( $values ); 646 | $compare_type = strtoupper( $compare_type ); 647 | 648 | if ( ! in_array( $compare_type, array( 'LIKE', 'NOT LIKE' ) ) ) { 649 | $compare_type = 'LIKE'; 650 | } 651 | 652 | $values = array_map( $callback, $values ); 653 | $value_count = count( $values ); 654 | 655 | $current = 0; 656 | 657 | // Escape values and build the SQL. 658 | foreach ( $values as $value ) { 659 | $value = $wpdb->prepare( '%s', $value ); 660 | 661 | $sql .= "`{$field}` {$compare_type} '%%{$value}%%'"; 662 | 663 | if ( $value_count > 1 && ++$current !== $value_count ) { 664 | $sql .= " {$operator} "; 665 | } 666 | } 667 | 668 | return $sql; 669 | } 670 | 671 | /** 672 | * Helper used by 'BETWEEN' comparison methods to build SQL. 673 | * 674 | * @since 1.0.0 675 | * 676 | * @param array $values Array of values to compare. 677 | * @param string|callable $callback_or_type Sanitization callback to pass values through, or shorthand 678 | * types to use preset callbacks. 679 | * @param string $compare_type Comparison to make. Accepts 'BETWEEN' or 'NOT BETWEEN'. 680 | * @return string Raw, sanitized SQL. 681 | */ 682 | protected function get_between_sql( $values, $callback_or_type, $compare_type ) { 683 | global $wpdb; 684 | 685 | $sql = ''; 686 | 687 | // Bail if `$values` isn't an array or there aren't at least two values. 688 | if ( ! is_array( $values ) || count( $values ) < 2 ) { 689 | return $sql; 690 | } 691 | 692 | $compare_type = strtoupper( $compare_type ); 693 | 694 | if ( ! in_array( $compare_type, array( 'BETWEEN', 'NOT BETWEEN' ) ) ) { 695 | $compare_type = 'BETWEEN'; 696 | } 697 | 698 | $field = $this->get_current_field(); 699 | $callback = $this->get_callback( $callback_or_type ); 700 | 701 | // Grab the first two values in the array. 702 | $values = array_slice( $values, 0, 2 ); 703 | 704 | // Sanitize the values according to the callback and cast dates. 705 | $values = array_map( function( $value ) use ( $callback, $wpdb ) { 706 | $value = call_user_func( $callback, $value ); 707 | 708 | if ( false !== strpos( $value, ':' ) ) { 709 | $value = $wpdb->prepare( '%s', $value ); 710 | $value = "CAST( {$value} AS DATE)"; 711 | } 712 | 713 | return $value; 714 | }, $values ); 715 | 716 | $sql .= "( `{$field}` {$compare_type} %s AND %s )"; 717 | 718 | return $wpdb->prepare( $sql, $values ); 719 | } 720 | 721 | /** 722 | * Retrieves the callback to use for the given type. 723 | * 724 | * @since 1.0.0 725 | * 726 | * @param string|callable $callback_or_type Standard type to retrieve a callback for, or an callback. 727 | * @return callable Callback. 728 | */ 729 | public function get_callback( $callback_or_type ) { 730 | 731 | $callback = is_callable( $callback_or_type ) ? $callback_or_type : $this->get_callback_for_type( $callback_or_type ); 732 | 733 | /** 734 | * Filters the callback to use for a given type. 735 | * 736 | * @since 1.0.0 737 | * 738 | * @param callable $callback Callback. 739 | * @param string $type Type to retrieve a callback for. 740 | * @param \Sandhills\Claws $this Current Sidebar instance. 741 | */ 742 | return apply_filters( 'claws_callback_for_type', $callback, $callback_or_type, $this ); 743 | } 744 | 745 | /** 746 | * Determines the right callback for a given type of value. 747 | * 748 | * @since 1.0.0 749 | * 750 | * @param string $type Type of value to retrieve a callback for. 751 | * @return string|callable Callback string. 752 | */ 753 | public function get_callback_for_type( $type ) { 754 | switch( $type ) { 755 | 756 | case 'int': 757 | case 'integer': 758 | $callback = 'intval'; 759 | break; 760 | 761 | case 'float': 762 | case 'double': 763 | $callback = 'floatval'; 764 | break; 765 | 766 | case 'string': 767 | $callback = 'sanitize_text_field'; 768 | break; 769 | 770 | case 'key': 771 | $callback = 'sanitize_key'; 772 | break; 773 | 774 | case 'esc_like': 775 | $callback = array( $this, 'esc_like' ); 776 | break; 777 | 778 | default: 779 | $callback = 'esc_sql'; 780 | break; 781 | } 782 | 783 | return $callback; 784 | } 785 | 786 | /** 787 | * Retrieves the CAST value for a given value type. 788 | * 789 | * @since 1.0.0 790 | * 791 | * @see WP_Meta_Query::get_cast_for_type() 792 | * 793 | * @param string $type Value type (as derived from gettype()). 794 | * @return string MySQL-ready CAST type. 795 | */ 796 | public function get_cast_for_type( $type ) { 797 | $type = strtoupper( $type ); 798 | 799 | if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|DOUBLE|INTEGER|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $type ) ) { 800 | return 'CHAR'; 801 | } 802 | 803 | if ( 'INTEGER' === $type || 'NUMERIC' === $type ) { 804 | $type = 'SIGNED'; 805 | } 806 | 807 | if ( 'DOUBLE' === $type ) { 808 | $type = 'DECIMAL'; 809 | } 810 | 811 | return $type; 812 | } 813 | 814 | /** 815 | * Validates and retrieves the operator. 816 | * 817 | * @since 1.0.0 818 | * 819 | * @param string $operator Operator. Accepts 'OR' or 'AND'. 820 | * @return string Operator. 'OR' if an invalid operator is passed to `$operator`. 821 | */ 822 | public function get_operator( $operator ) { 823 | $operator = strtoupper( $operator ); 824 | 825 | if ( ! in_array( $operator, array( 'OR', 'AND' ) ) ) { 826 | $operator = 'OR'; 827 | } 828 | 829 | return $operator; 830 | } 831 | 832 | /** 833 | * Escapes a value used in a 'LIKE' comparison. 834 | * 835 | * @since 1.0.0 836 | * 837 | * @param mixed $like LIKE comparison value. 838 | * @return string Escaped value. 839 | */ 840 | protected function esc_like( $like ) { 841 | return addcslashes( $like, '_%\\' ); 842 | } 843 | 844 | /** 845 | * Ensures values are in array form. 846 | * 847 | * Seems silly, but anywhere blatant duplication can be reduced is a win. 848 | * 849 | * @since 1.0.0 850 | * 851 | * @param mixed|array $values Single values of varying type or an array of values. 852 | * @return array Array of values. 853 | */ 854 | protected function prepare_values( $values ) { 855 | return is_array( $values ) ? $values : (array) $values; 856 | } 857 | 858 | /** 859 | * Replaces the previous phrase with the given prepared SQL. 860 | * 861 | * @since 1.0.0 862 | * 863 | * @param string $sql Prepared SQL to replace the phrase with. 864 | * @param null|string $clause Optional. Clause to replace the last phrase for. Default is the current clause. 865 | */ 866 | protected function replace_previous_phrase( $sql, $clause = null ) { 867 | $clause = $this->get_clause( $clause ); 868 | 869 | // Pop off the last phrase. 870 | array_pop( $this->clauses_in_progress[ $clause ] ); 871 | 872 | // Replace it with the new one. 873 | $this->clauses_in_progress[ $clause ][] = $sql; 874 | } 875 | 876 | /** 877 | * Adds prepared SQL to the current clause. 878 | * 879 | * @since 1.0.0 880 | * 881 | * @param string $sql Prepared SQL to add to the clause. 882 | * @param null|string $clause Optional. Clause to add the SQL to. Default is the current clause. 883 | */ 884 | public function add_clause_sql( $sql, $clause = null ) { 885 | $clause = $this->get_clause( $clause ); 886 | 887 | if ( true === $this->amending_previous ) { 888 | $operator = $this->get_current_operator(); 889 | 890 | $sql = $this->get_previous_phrase() . " {$operator} {$sql}"; 891 | 892 | $this->replace_previous_phrase( $sql, $clause ); 893 | 894 | // Reset the amendment flag. 895 | $this->amending_previous = false; 896 | 897 | $this->previous_phrase = $sql; 898 | } else { 899 | $this->previous_phrase = $sql; 900 | $this->clauses_in_progress[ $clause ][] = $this->previous_phrase; 901 | } 902 | 903 | } 904 | 905 | /** 906 | * Retrieves raw, sanitized SQL for the current clause. 907 | * 908 | * @since 1.0.0 909 | * 910 | * @param null|string $clause Optional. Clause to build SQL for. Default is the current clause. 911 | * @param bool $reset_vars Optional. Whether to reset the clause, field, and operator vars 912 | * after retrieving the clause's SQL. Default true. 913 | * @return string Raw, sanitized SQL. 914 | */ 915 | public function get_sql( $clause = null, $reset_vars = true ) { 916 | $sql = ''; 917 | 918 | $clause = $this->get_clause( $clause ); 919 | 920 | if ( isset( $this->clauses_in_progress[ $clause ] ) ) { 921 | $sql .= strtoupper( $clause ); 922 | 923 | $current = 0; 924 | 925 | foreach ( $this->clauses_in_progress[ $clause ] as $chunk ) { 926 | if ( ++$current === 1 ) { 927 | $sql .= " {$chunk}"; 928 | } elseif( $current >= 2 ) { 929 | $sql .= " AND {$chunk}"; 930 | } 931 | } 932 | 933 | if ( true === $reset_vars ) { 934 | $this->reset_vars(); 935 | } 936 | } 937 | 938 | return $sql; 939 | } 940 | 941 | /** 942 | * Sets the current clause. 943 | * 944 | * @since 1.0.0 945 | * 946 | * @param string $clause Clause to set as current. 947 | * @return \Sandhills\Claws Current claws instance. 948 | */ 949 | public function set_current_clause( $clause ) { 950 | $clause = strtolower( $clause ); 951 | 952 | if ( in_array( $clause, $this->allowed_clauses, true ) ) { 953 | $this->current_clause = $clause; 954 | } 955 | 956 | return $this; 957 | } 958 | 959 | /** 960 | * Retrieves the current clause. 961 | * 962 | * @since 1.0.0 963 | * 964 | * @param null|string $clause Optional. Clause to retrieve. Default is the current clause. 965 | * @return string Current clause name. 966 | */ 967 | public function get_clause( $clause = null ) { 968 | if ( ! isset( $clause ) || ! in_array( $clause, $this->allowed_clauses, true ) ) { 969 | $clause = $this->current_clause; 970 | } 971 | 972 | return $clause; 973 | } 974 | 975 | /** 976 | * Sets the current field. 977 | * 978 | * @since 1.0.0 979 | * 980 | * @param string $field Field to set as current. 981 | * @return \Sandhills\Claws Current claws instance. 982 | */ 983 | public function set_current_field( $field ) { 984 | if ( $field !== $this->get_current_field() ) { 985 | $this->current_field = sanitize_key( $field ); 986 | } 987 | 988 | return $this; 989 | } 990 | 991 | /** 992 | * Retrieves the current field name. 993 | * 994 | * @since 1.0.0 995 | * 996 | * @return string Current field name. 997 | */ 998 | public function get_current_field() { 999 | return $this->current_field; 1000 | } 1001 | 1002 | /** 1003 | * Sets the current operator for use in complex phrase building. 1004 | * 1005 | * @since 1.0.0 1006 | * 1007 | * @param string $operator Operator to persist between method calls. Accepts 'OR' or 'AND'. 1008 | * @return \Sandhills\Claws Current claws instance. 1009 | */ 1010 | public function set_current_operator( $operator ) { 1011 | $operator = $this->get_operator( $operator ); 1012 | 1013 | $this->current_operator = $operator; 1014 | 1015 | return $this; 1016 | } 1017 | 1018 | /** 1019 | * Flags the previously-stored phrase to be amended and appended with the given operator. 1020 | * 1021 | * @since 1.0.0 1022 | * 1023 | * @param null|string $clause Optional. Clause to amend the previous chunk for. 1024 | * Default is the current clause. 1025 | * @return \Sandhills\Claws Current Claws instance. 1026 | */ 1027 | private function __set_current_operator( $operator, $clause ) { 1028 | $operator = strtoupper( $operator ); 1029 | 1030 | if ( ! in_array( $operator, array( 'OR', 'AND' ) ) ) { 1031 | $operator = 'OR'; 1032 | } 1033 | 1034 | $this->set_current_operator( $operator ); 1035 | $this->amending_previous = true; 1036 | 1037 | $clause = $this->get_clause( $clause ); 1038 | $chunks = $this->clauses_in_progress[ $clause ]; 1039 | 1040 | if ( ! empty( $chunks ) ) { 1041 | $this->previous_phrase = end( $chunks ); 1042 | } 1043 | 1044 | return $this; 1045 | } 1046 | 1047 | /** 1048 | * Retrieves the current operator (for use in complex phrase building). 1049 | * 1050 | * @since 1.0.0 1051 | * 1052 | * @return string Current operator. 1053 | */ 1054 | public function get_current_operator() { 1055 | return $this->current_operator; 1056 | } 1057 | 1058 | /** 1059 | * Retrieves the previous phrase for the given clause. 1060 | * 1061 | * @since 1.0.0 1062 | * 1063 | * @return string Previous phrase SQL. 1064 | */ 1065 | public function get_previous_phrase() { 1066 | return $this->previous_phrase; 1067 | } 1068 | 1069 | /** 1070 | * Resets the current clause, field, and operator. 1071 | * 1072 | * @since 1.0.0 1073 | */ 1074 | public function reset_vars() { 1075 | $this->current_clause = null; 1076 | $this->current_field = null; 1077 | $this->current_operator = null; 1078 | } 1079 | } 1080 | } 1081 | 1082 | namespace { 1083 | 1084 | /** 1085 | * Shorthand helper for retrieving a Claws instance. 1086 | * 1087 | * @since 1.0.0 1088 | * 1089 | * @return \Sandhills\Claws Claws instance. 1090 | */ 1091 | function claws() { 1092 | return new \Sandhills\Claws; 1093 | } 1094 | 1095 | } 1096 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------