1<?php 2/** 3 * @file handlers.inc 4 * Defines the various handler objects to help build and display views. 5 */ 6 7/** 8 * Instantiate and construct a new handler 9 */ 10function _views_create_handler($definition, $type = 'handler') { 11// vpr('Instantiating handler ' . $definition['handler']); 12 if (empty($definition['handler'])) { 13 return; 14 } 15 16 if (!class_exists($definition['handler']) && !views_include_handler($definition, $type)) { 17 return; 18 } 19 20 $handler = new $definition['handler']; 21 $handler->set_definition($definition); 22 // let the handler have something like a constructor. 23 $handler->construct(); 24 25 return $handler; 26} 27 28/** 29 * Attempt to find the include file for a given handler from its definition. 30 * 31 * This will also attempt to include all parents, though we're maxing the 32 * parent chain to 10 to prevent infinite loops. 33 */ 34function views_include_handler($definition, $type, $count = 0) { 35 // Do not proceed if the class already exists. 36 if (isset($definition['handler']) && class_exists($definition['handler'])) { 37 return TRUE; 38 } 39 40 // simple infinite loop prevention. 41 if ($count > 10) { 42 vpr(t('Handler @handler include tried to loop infinitely!', array('@handler' => $definition['handler']))); 43 return FALSE; 44 } 45 46 if (!isset($definition['path'])) { 47 if ($type == 'handler') { 48 $definition += views_fetch_handler_data($definition['handler']); 49 } 50 else { 51 $definition += views_fetch_plugin_data($type, $definition['handler']); 52 } 53 } 54 55 if (!empty($definition['parent'])) { 56 if ($type == 'handler') { 57 $parent = views_fetch_handler_data($definition['parent']); 58 } 59 else { 60 $parent = views_fetch_plugin_data($type, $definition['parent']); 61 } 62 63 if ($parent) { 64 $rc = views_include_handler($parent, $type, $count + 1); 65 // If the parent chain cannot be included, don't try; this will 66 // help alleviate problems with modules with cross dependencies. 67 if (!$rc) { 68 return FALSE; 69 } 70 } 71 } 72 73 if (isset($definition['path']) && $definition['file']) { 74 $filename = './' . $definition['path'] . '/' . $definition['file']; 75 if (file_exists($filename)) { 76 require_once $filename; 77 } 78 } 79 80 return class_exists($definition['handler']); 81} 82 83/** 84 * Prepare a handler's data by checking defaults and such. 85 */ 86function _views_prepare_handler($definition, $data, $field) { 87 foreach (array('group', 'title', 'title short', 'help', 'real field') as $key) { 88 if (!isset($definition[$key])) { 89 // First check the field level 90 if (!empty($data[$field][$key])) { 91 $definition[$key] = $data[$field][$key]; 92 } 93 // Then if that doesn't work, check the table level 94 else if (!empty($data['table'][$key])) { 95 $definition[$key] = $data['table'][$key]; 96 } 97 } 98 } 99 100 return _views_create_handler($definition); 101} 102 103/** 104 * Fetch the handler data from cache. 105 */ 106function views_fetch_handler_data($handler = NULL) { 107 static $cache = NULL; 108 if (!isset($cache)) { 109 $start = views_microtime(); 110 111 $cache = views_discover_handlers(); 112 113 vpr('Views handlers build time: ' . (views_microtime() - $start) * 1000 . ' ms'); 114 } 115 116 if (!$handler) { 117 return $cache; 118 } 119 else if (isset($cache[$handler])) { 120 return $cache[$handler]; 121 } 122 123 // Return an empty array if there is no match. 124 return array(); 125} 126 127/** 128 * Builds and return a list of all handlers available in the system. 129 * 130 * @return Nested array of handlers 131 */ 132function views_discover_handlers() { 133 $cache = array(); 134 // Get handlers from all modules. 135 foreach (module_implements('views_handlers') as $module) { 136 $function = $module . '_views_handlers'; 137 $result = $function(); 138 if (!is_array($result)) { 139 continue; 140 } 141 142 $module_dir = isset($result['info']['module']) ? $result['info']['module'] : $module; 143 $path = isset($result['info']['path']) ? $result['info']['path'] : drupal_get_path('module', $module_dir); 144 145 foreach ($result['handlers'] as $handler => $def) { 146 if (!isset($def['module'])) { 147 $def['module'] = $module_dir; 148 } 149 if (!isset($def['path'])) { 150 $def['path'] = $path; 151 } 152 if (!isset($def['file'])) { 153 $def['file'] = "$handler.inc"; 154 } 155 if (!isset($def['handler'])) { 156 $def['handler'] = $handler; 157 } 158 // merge the new data in 159 $cache[$handler] = $def; 160 } 161 } 162 return $cache; 163} 164 165/** 166 * Fetch a handler to join one table to a primary table from the data cache 167 */ 168function views_get_table_join($table, $base_table) { 169 $data = views_fetch_data($table); 170 if (isset($data['table']['join'][$base_table])) { 171 $h = $data['table']['join'][$base_table]; 172 if (!empty($h['handler']) && class_exists($h['handler'])) { 173 $handler = new $h['handler']; 174 } 175 else { 176 $handler = new views_join(); 177 } 178 179 // Fill in some easy defaults 180 $handler->definition = $h; 181 if (empty($handler->definition['table'])) { 182 $handler->definition['table'] = $table; 183 } 184 // If this is empty, it's a direct link. 185 if (empty($handler->definition['left_table'])) { 186 $handler->definition['left_table'] = $base_table; 187 } 188 189 if (isset($h['arguments'])) { 190 call_user_func_array(array(&$handler, 'construct'), $h['arguments']); 191 } 192 else { 193 $handler->construct(); 194 } 195 196 return $handler; 197 } 198 // DEBUG -- identify missing handlers 199 vpr("Missing join: $table $base_table"); 200} 201 202/** 203 * Base handler, from which all the other handlers are derived. 204 * It creates a common interface to create consistency amongst 205 * handlers and data. 206 * 207 * This class would be abstract in PHP5, but PHP4 doesn't understand that. 208 * 209 * Definition terms: 210 * - table: The actual table this uses; only specify if different from 211 * the table this is attached to. 212 * - real field: The actual field this uses; only specify if different from 213 * the field this item is attached to. 214 * - group: A text string representing the 'group' this item is attached to, 215 * for display in the UI. Examples: "Node", "Taxonomy", "Comment", 216 * "User", etc. This may be inherited from the parent definition or 217 * the 'table' definition. 218 * - title: The title for this handler in the UI. This may be inherited from 219 * the parent definition or the 'table' definition. 220 * - help: A more informative string to give to the user to explain what this 221 * field/handler is or does. 222 * - access callback: If this field should have access control, this could 223 * be a function to use. 'user_access' is a common 224 * function to use here. If not specified, no access 225 * control is provided. 226 * - access arguments: An array of arguments for the access callback. 227 */ 228class views_handler extends views_object { 229 /** 230 * init the handler with necessary data. 231 * @param $view 232 * The $view object this handler is attached to. 233 * @param $options 234 * The item from the database; the actual contents of this will vary 235 * based upon the type of handler. 236 */ 237 function init(&$view, $options) { 238 $this->view = &$view; 239 $this->unpack_options($this->options, $options); 240 241 // This exist on most handlers, but not all. So they are still optional. 242 if (isset($options['table'])) { 243 $this->table = $options['table']; 244 } 245 246 if (isset($this->definition['real field'])) { 247 $this->real_field = $this->definition['real field']; 248 } 249 250 if (isset($this->definition['field'])) { 251 $this->real_field = $this->definition['field']; 252 } 253 254 if (isset($options['field'])) { 255 $this->field = $options['field']; 256 if (!isset($this->real_field)) { 257 $this->real_field = $options['field']; 258 } 259 } 260 261 $this->query = &$view->query; 262 } 263 264 /** 265 * Return a string representing this handler's name in the UI. 266 */ 267 function ui_name($short = FALSE) { 268 $title = ($short && isset($this->definition['title short'])) ? $this->definition['title short'] : $this->definition['title']; 269 return t('!group: !title', array('!group' => $this->definition['group'], '!title' => $title)); 270 } 271 272 /** 273 * Provide a form for setting options. 274 */ 275 function options_form(&$form, &$form_state) { } 276 277 /** 278 * Validate the options form. 279 */ 280 function options_validate($form, &$form_state) { } 281 282 /** 283 * Perform any necessary changes to the form values prior to storage. 284 * There is no need for this function to actually store the data. 285 */ 286 function options_submit($form, &$form_state) { } 287 288 /** 289 * If a handler has 'extra options' it will get a little settings widget and 290 * another form called extra_options. 291 */ 292 function has_extra_options() { return FALSE; } 293 294 /** 295 * Provide defaults for the handler. 296 */ 297 function extra_options(&$option) { } 298 299 /** 300 * Provide a form for setting options. 301 */ 302 function extra_options_form(&$form, &$form_state) { } 303 304 /** 305 * Validate the options form. 306 */ 307 function extra_options_validate($form, &$form_state) { } 308 309 /** 310 * Perform any necessary changes to the form values prior to storage. 311 * There is no need for this function to actually store the data. 312 */ 313 function extra_options_submit($form, &$form_state) { } 314 315 /** 316 * Set new exposed option defaults when exposed setting is flipped 317 * on. 318 */ 319 function expose_options() { } 320 /** 321 * Render our chunk of the exposed filter form when selecting 322 */ 323 function exposed_form(&$form, &$form_state) { } 324 325 /** 326 * Validate the exposed filter form 327 */ 328 function exposed_validate(&$form, &$form_state) { } 329 330 /** 331 * Submit the exposed filter form 332 */ 333 function exposed_submit(&$form, &$form_state) { } 334 335 /** 336 * Get information about the exposed form for the form renderer. 337 * 338 * @return 339 * An array with the following keys: 340 * - operator: The $form key of the operator. Set to NULL if no operator. 341 * - value: The $form key of the value. Set to NULL if no value. 342 * - label: The label to use for this piece. 343 */ 344 function exposed_info() { } 345 346 /** 347 * Determine if a handler can be exposed. 348 */ 349 function can_expose() { return FALSE; } 350 351 /** 352 * Check whether current user has access to this handler. 353 * 354 * @return boolean 355 */ 356 function access() { 357 if (isset($this->definition['access callback']) && function_exists($this->definition['access callback'])) { 358 if (isset($this->definition['access arguments']) && is_array($this->definition['access arguments'])) { 359 return call_user_func_array($this->definition['access callback'], $this->definition['access arguments']); 360 } 361 return $this->definition['access callback'](); 362 } 363 364 return TRUE; 365 } 366 367 /** 368 * Run before the view is built. 369 * 370 * This gives all the handlers some time to set up before any handler has 371 * been fully run. 372 */ 373 function pre_query() { } 374 375 /** 376 * Called just prior to query(), this lets a handler set up any relationship 377 * it needs. 378 */ 379 function set_relationship() { 380 // Ensure this gets set to something. 381 $this->relationship = NULL; 382 383 // Don't process non-existant relationships. 384 if (empty($this->options['relationship']) || $this->options['relationship'] == 'none') { 385 return; 386 } 387 388 $relationship = $this->options['relationship']; 389 390 // Ignore missing/broken relationships. 391 if (empty($this->view->relationship[$relationship])) { 392 return; 393 } 394 395 // Check to see if the relationship has already processed. If not, then we 396 // cannot process it. 397 if (empty($this->view->relationship[$relationship]->alias)) { 398 return; 399 } 400 401 // Finally! 402 $this->relationship = $this->view->relationship[$relationship]->alias; 403 } 404 405 /** 406 * Add this handler into the query. 407 * 408 * If we were using PHP5, this would be abstract. 409 */ 410 function query() { } 411 412 /** 413 * Ensure the main table for this handler is in the query. This is used 414 * a lot. 415 */ 416 function ensure_my_table() { 417 if (!isset($this->table_alias)) { 418 $this->table_alias = $this->query->ensure_table($this->table, $this->relationship); 419 } 420 return $this->table_alias; 421 } 422 423 /** 424 * Provide text for the administrative summary 425 */ 426 function admin_summary() { } 427 428 /** 429 * Determine if the argument needs a style plugin. 430 * 431 * @return TRUE/FALSE 432 */ 433 function needs_style_plugin() { return FALSE; } 434 435 /** 436 * Determine if this item is 'exposed', meaning it provides form elements 437 * to let users modify the view. 438 * 439 * @return TRUE/FALSE 440 */ 441 function is_exposed() { 442 return !empty($this->options['exposed']); 443 } 444 445 /** 446 * Take input from exposed filters and assign to this handler, if necessary. 447 */ 448 function accept_exposed_input($input) { return TRUE; } 449 450 /** 451 * If set to remember exposed input in the session, store it there. 452 */ 453 function store_exposed_input($input, $status) { return TRUE; } 454 455 /** 456 * Get the join object that should be used for this handler. 457 * 458 * This method isn't used a great deal, but it's very handy for easily 459 * getting the join if it is necessary to make some changes to it, such 460 * as adding an 'extra'. 461 */ 462 function get_join() { 463 // get the join from this table that links back to the base table. 464 // Determine the primary table to seek 465 if (empty($this->query->relationships[$this->relationship])) { 466 $base_table = $this->query->base_table; 467 } 468 else { 469 $base_table = $this->query->relationships[$this->relationship]['base']; 470 } 471 472 $join = views_get_table_join($this->table, $base_table); 473 if ($join) { 474 return drupal_clone($join); 475 } 476 } 477 478 /** 479 * Validates the handler against the complete View. 480 * 481 * This is called when the complete View is being validated. For validating 482 * the handler options form use options_validate(). 483 * 484 * @see views_handler::options_validate() 485 * 486 * @return 487 * Empty array if the handler is valid; an array of error strings if it is not. 488 */ 489 function validate() { return array(); } 490 491 /** 492 * Determine if the handler is considered 'broken', meaning it's a 493 * a placeholder used when a handler can't be found. 494 */ 495 function broken() { } 496} 497 498/** 499 * This many to one helper object is used on both arguments and filters. 500 * 501 * @todo This requires extensive documentation on how this class is to 502 * be used. For now, look at the arguments and filters that use it. Lots 503 * of stuff is just pass-through but there are definitely some interesting 504 * areas where they interact. 505 * 506 * Any handler that uses this can have the following possibly additional 507 * definition terms: 508 * - numeric: If true, treat this field as numeric, using %d instead of %s in 509 * queries. 510 * 511 */ 512class views_many_to_one_helper { 513 function views_many_to_one_helper(&$handler) { 514 $this->handler = &$handler; 515 } 516 517 public static function option_definition(&$options) { 518 $options['reduce_duplicates'] = array('default' => FALSE); 519 } 520 521 function options_form(&$form, &$form_state) { 522 $form['reduce_duplicates'] = array( 523 '#type' => 'checkbox', 524 '#title' => t('Reduce duplicates'), 525 '#description' => t('This filter can cause items that have more than one of the selected options to appear as duplicate results. If this filter causes duplicate results to occur, this checkbox can reduce those duplicates; however, the more terms it has to search for, the less performant the query will be, so use this with caution. Shouldn\'t be set on single-value fields, as it may cause values to disappear from display, if used on an incompatible field.'), 526 '#default_value' => !empty($this->handler->options['reduce_duplicates']), 527 ); 528 } 529 530 /** 531 * Sometimes the handler might want us to use some kind of formula, so give 532 * it that option. If it wants us to do this, it must set $helper->formula = TRUE 533 * and implement handler->get_formula(); 534 */ 535 function get_field() { 536 if (!empty($this->formula)) { 537 return $this->handler->get_formula(); 538 } 539 else { 540 return $this->handler->table_alias . '.' . $this->handler->real_field; 541 } 542 } 543 544 /** 545 * Add a table to the query. 546 * 547 * This is an advanced concept; not only does it add a new instance of the table, 548 * but it follows the relationship path all the way down to the relationship 549 * link point and adds *that* as a new relationship and then adds the table to 550 * the relationship, if necessary. 551 */ 552 function add_table($join = NULL, $alias = NULL) { 553 // This is used for lookups in the many_to_one table. 554 $field = $this->handler->table . '.' . $this->handler->field; 555 556 if (empty($join)) { 557 $join = $this->get_join(); 558 } 559 560 // See if there's a chain between us and the base relationship. If so, we need 561 // to create a new relationship to use. 562 $relationship = $this->handler->relationship; 563 564 // Determine the primary table to seek 565 if (empty($this->handler->query->relationships[$relationship])) { 566 $base_table = $this->handler->query->base_table; 567 } 568 else { 569 $base_table = $this->handler->query->relationships[$relationship]['base']; 570 } 571 572 // Cycle through the joins. This isn't as error-safe as the normal 573 // ensure_path logic. Perhaps it should be. 574 $r_join = drupal_clone($join); 575 while ($r_join->left_table != $base_table) { 576 $r_join = views_get_table_join($r_join->left_table, $base_table); 577 } 578 // If we found that there are tables in between, add the relationship. 579 if ($r_join->table != $join->table) { 580 $relationship = $this->handler->query->add_relationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship); 581 } 582 583 // And now add our table, using the new relationship if one was used. 584 $alias = $this->handler->query->add_table($this->handler->table, $relationship, $join, $alias); 585 586 // Store what values are used by this table chain so that other chains can 587 // automatically discard those values. 588 if (empty($this->handler->view->many_to_one_tables[$field])) { 589 $this->handler->view->many_to_one_tables[$field] = $this->handler->value; 590 } 591 else { 592 $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value); 593 } 594 595 return $alias; 596 } 597 598 function get_join() { 599 return $this->handler->get_join(); 600 } 601 602 /** 603 * Provide the proper join for summary queries. This is important in part because 604 * it will cooperate with other arguments if possible. 605 */ 606 function summary_join() { 607 $field = $this->handler->table . '.' . $this->handler->field; 608 $join = $this->get_join(); 609 610 // shortcuts 611 $options = $this->handler->options; 612 $view = &$this->handler->view; 613 $query = &$this->handler->query; 614 615 if (!empty($options['require_value'])) { 616 $join->type = 'INNER'; 617 } 618 619 if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) { 620 return $query->ensure_table($this->handler->table, $this->handler->relationship, $join); 621 } 622 else { 623 if (!empty($view->many_to_one_tables[$field])) { 624 foreach ($view->many_to_one_tables[$field] as $value) { 625 $join->extra = array( 626 array( 627 'field' => $this->handler->real_field, 628 'operator' => '!=', 629 'value' => $value, 630 'numeric' => !empty($this->definition['numeric']), 631 ), 632 ); 633 } 634 } 635 return $this->add_table($join); 636 } 637 } 638 639 /** 640 * Override ensure_my_table so we can control how this joins in. 641 * The operator actually has influence over joining. 642 */ 643 function ensure_my_table() { 644 if (!isset($this->handler->table_alias)) { 645 // For 'or' if we're not reducing duplicates, we get the absolute simplest: 646 $field = $this->handler->table . '.' . $this->handler->field; 647 if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) { 648 if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) { 649 // query optimization, INNER joins are slightly faster, so use them 650 // when we know we can. 651 $join = $this->get_join(); 652 $join->type = 'INNER'; 653 $this->handler->table_alias = $this->handler->query->ensure_table($this->handler->table, $this->handler->relationship, $join); 654 $this->handler->view->many_to_one_tables[$field] = $this->handler->value; 655 } 656 else { 657 $join = $this->get_join(); 658 $join->type = 'LEFT'; 659 if (!empty($this->handler->view->many_to_one_tables[$field])) { 660 foreach ($this->handler->view->many_to_one_tables[$field] as $value) { 661 $join->extra = array( 662 array( 663 'field' => $this->handler->real_field, 664 'operator' => '!=', 665 'value' => $value, 666 'numeric' => !empty($this->handler->definition['numeric']), 667 ), 668 ); 669 } 670 } 671 672 $this->handler->table_alias = $this->add_table($join); 673 } 674 675 return $this->handler->table_alias; 676 } 677 678 if ($this->handler->operator != 'not') { 679 // If it's an and or an or, we do one join per selected value. 680 // Clone the join for each table: 681 $this->handler->table_aliases = array(); 682 foreach ($this->handler->value as $value) { 683 $join = $this->get_join(); 684 if ($this->handler->operator == 'and') { 685 $join->type = 'INNER'; 686 } 687 $join->extra = array( 688 array( 689 'field' => $this->handler->real_field, 690 'value' => $value, 691 'numeric' => !empty($this->handler->definition['numeric']), 692 ), 693 ); 694 695 // The table alias needs to be unique to this value across the 696 // multiple times the filter or argument is called by the view. 697 if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) { 698 if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) { 699 $this->handler->view->many_to_one_count[$this->handler->table] = 0; 700 } 701 $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++); 702 } 703 $alias = $this->handler->table_aliases[$value] = $this->add_table($join, $this->handler->view->many_to_one_aliases[$field][$value]); 704 705 // and set table_alias to the first of these. 706 if (empty($this->handler->table_alias)) { 707 $this->handler->table_alias = $alias; 708 } 709 } 710 } 711 else { 712 // For not, we just do one join. We'll add a where clause during 713 // the query phase to ensure that $table.$field IS NULL. 714 $join = $this->get_join(); 715 $join->type = 'LEFT'; 716 $join->extra = array(); 717 $join->extra_type = 'OR'; 718 foreach ($this->handler->value as $value) { 719 $join->extra[] = array( 720 'field' => $this->handler->real_field, 721 'value' => $value, 722 'numeric' => !empty($this->handler->definition['numeric']), 723 ); 724 } 725 726 $this->handler->table_alias = $this->add_table($join); 727 } 728 } 729 return $this->handler->table_alias; 730 } 731 732 function add_filter() { 733 if (empty($this->handler->value)) { 734 return; 735 } 736 $this->handler->ensure_my_table(); 737 738 // Shorten some variables: 739 $field = $this->get_field(); 740 $options = $this->handler->options; 741 $operator = $this->handler->operator; 742 if (empty($options['group'])) { 743 $options['group'] = 0; 744 } 745 746 $placeholder = !empty($this->handler->definition['numeric']) ? '%d' : "'%s'"; 747 748 if ($operator == 'not') { 749 $this->handler->query->add_where($options['group'], "$field IS NULL"); 750 } 751 else if ($operator == 'or' && empty($options['reduce_duplicates'])) { 752 if (count($this->handler->value) > 1) { 753 $replace = array_fill(0, sizeof($this->handler->value), $placeholder); 754 $in = '(' . implode(", ", $replace) . ')'; 755 $this->handler->query->add_where($options['group'], "$field IN $in", $this->handler->value); 756 } 757 else { 758 $this->handler->query->add_where($options['group'], "$field = $placeholder", $this->handler->value); 759 } 760 } 761 else { 762 $field = $this->handler->real_field; 763 $clauses = array(); 764 foreach ($this->handler->table_aliases as $value => $alias) { 765 $clauses[] = "$alias.$field = $placeholder"; 766 } 767 768 $group = empty($options['group']) ? 0 : $options['group']; 769 770 // implode on either AND or OR. 771 $this->handler->query->add_where($group, implode(' ' . strtoupper($operator) . ' ', $clauses), $this->handler->value); 772 } 773 } 774} 775 776/* 777 * Break x,y,z and x+y+z into an array. Numeric only. 778 * 779 * @param $str 780 * The string to parse. 781 * @param $filter 782 * The filter object to use as a base. 783 * 784 * @return $filter 785 * The new filter object. 786 */ 787function views_break_phrase($str, &$filter) { 788 if (!$filter) { 789 $filter = new stdClass(); 790 } 791 792 // Set up defaults: 793 if (!isset($filter->value)) { 794 $filter->value = array(); 795 } 796 797 if (!isset($filter->operator)) { 798 $filter->operator = 'or'; 799 } 800 801 if ($str == '') { 802 return $filter; 803 } 804 805 if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) { 806 // The '+' character in a query string may be parsed as ' '. 807 $filter->operator = 'or'; 808 $filter->value = preg_split('/[+ ]/', $str); 809 } 810 else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) { 811 $filter->operator = 'and'; 812 $filter->value = explode(',', $str); 813 } 814 815 // Keep an 'error' value if invalid strings were given. 816 if (!empty($str) && (empty($filter->value) || !is_array($filter->value))) { 817 $filter->value = array(-1); 818 return $filter; 819 } 820 821 // Doubly ensure that all values are numeric only. 822 foreach ($filter->value as $id => $value) { 823 $filter->value[$id] = intval($value); 824 } 825 826 return $filter; 827} 828 829// -------------------------------------------------------------------------- 830// Date helper functions 831 832/** 833 * Figure out what timezone we're in; needed for some date manipulations. 834 */ 835function views_get_timezone() { 836 global $user; 837 if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) { 838 $timezone = $user->timezone; 839 } 840 else { 841 $timezone = variable_get('date_default_timezone', 0); 842 } 843 844 // set up the database timezone 845 if (in_array($GLOBALS['db_type'], array('mysql', 'mysqli', 'pgsql'))) { 846 $offset = '+00:00'; 847 static $already_set = false; 848 if (!$already_set) { 849 if ($GLOBALS['db_type'] == 'pgsql') { 850 db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE"); 851 } 852 elseif ($GLOBALS['db_type'] == 'mysqli') { 853 db_query("SET @@session.time_zone = '$offset'"); 854 } 855 elseif ($GLOBALS['db_type'] == 'mysql' && version_compare(db_version(), '4.1.3', '>=')) { 856 db_query("SET @@session.time_zone = '$offset'"); 857 } 858 859 $already_set = true; 860 } 861 } 862 863 return $timezone; 864} 865 866/** 867 * Helper function to create cross-database SQL dates. 868 * 869 * @param $field 870 * The real table and field name, like 'tablename.fieldname'. 871 * @param $field_type 872 * The type of date field, 'int' or 'datetime'. 873 * @param $set_offset 874 * The name of a field that holds the timezone offset or a fixed timezone 875 * offset value. If not provided, the normal Drupal timezone handling 876 * will be used, i.e. $set_offset = 0 will make no timezone adjustment. 877 * @return 878 * An appropriate SQL string for the db type and field type. 879 */ 880function views_date_sql_field($field, $field_type = 'int', $set_offset = NULL) { 881 $db_type = $GLOBALS['db_type']; 882 $offset = $set_offset !== NULL ? $set_offset : views_get_timezone(); 883 switch ($db_type) { 884 case 'mysql': 885 case 'mysqli': 886 switch ($field_type) { 887 case 'int': 888 $field = "FROM_UNIXTIME($field)"; 889 break; 890 case 'datetime': 891 break; 892 } 893 if (!empty($offset)) { 894 $field = "($field + INTERVAL $offset SECOND)"; 895 } 896 return $field; 897 case 'pgsql': 898 switch ($field_type) { 899 case 'int': 900 $field = "$field::ABSTIME"; 901 break; 902 case 'datetime': 903 break; 904 } 905 if (!empty($offset)) { 906 $field = "($field + INTERVAL '$offset SECONDS')"; 907 } 908 return $field; 909 } 910} 911 912/** 913 * Helper function to create cross-database SQL date formatting. 914 * 915 * @param $format 916 * A format string for the result, like 'Y-m-d H:i:s'. 917 * @param $field 918 * The real table and field name, like 'tablename.fieldname'. 919 * @param $field_type 920 * The type of date field, 'int' or 'datetime'. 921 * @param $set_offset 922 * The name of a field that holds the timezone offset or a fixed timezone 923 * offset value. If not provided, the normal Drupal timezone handling 924 * will be used, i.e. $set_offset = 0 will make no timezone adjustment. 925 * @return 926 * An appropriate SQL string for the db type and field type. 927 */ 928function views_date_sql_format($format, $field, $field_type = 'int', $set_offset = NULL) { 929 $db_type = $GLOBALS['db_type']; 930 $field = views_date_sql_field($field, $field_type, $set_offset); 931 switch ($db_type) { 932 case 'mysql': 933 case 'mysqli': 934 $replace = array( 935 'Y' => '%Y', 936 'y' => '%y', 937 'M' => '%%b', 938 'm' => '%m', 939 'n' => '%c', 940 'F' => '%M', 941 'D' => '%a', 942 'd' => '%%d', 943 'l' => '%W', 944 'j' => '%e', 945 'W' => '%v', 946 'H' => '%H', 947 'h' => '%h', 948 'i' => '%i', 949 's' => '%%s', 950 'A' => '%p', 951 ); 952 $format = strtr($format, $replace); 953 return "DATE_FORMAT($field, '$format')"; 954 case 'pgsql': 955 $replace = array( 956 'Y' => 'YYYY', 957 'y' => 'YY', 958 'M' => 'Mon', 959 'm' => 'MM', 960 'n' => 'MM', // no format for Numeric representation of a month, without leading zeros 961 'F' => 'Month', 962 'D' => 'Dy', 963 'd' => 'DD', 964 'l' => 'Day', 965 'j' => 'DD', // no format for Day of the month without leading zeros 966 'W' => 'WW', 967 'H' => 'HH24', 968 'h' => 'HH12', 969 'i' => 'MI', 970 's' => 'SS', 971 'A' => 'AM', 972 ); 973 $format = strtr($format, $replace); 974 return "TO_CHAR($field, '$format')"; 975 } 976} 977 978/** 979 * Helper function to create cross-database SQL date extraction. 980 * 981 * @param $extract_type 982 * The type of value to extract from the date, like 'MONTH'. 983 * @param $field 984 * The real table and field name, like 'tablename.fieldname'. 985 * @param $field_type 986 * The type of date field, 'int' or 'datetime'. 987 * @param $set_offset 988 * The name of a field that holds the timezone offset or a fixed timezone 989 * offset value. If not provided, the normal Drupal timezone handling 990 * will be used, i.e. $set_offset = 0 will make no timezone adjustment. 991 * @return 992 * An appropriate SQL string for the db type and field type. 993 */ 994function views_date_sql_extract($extract_type, $field, $field_type = 'int', $set_offset = NULL) { 995 $db_type = $GLOBALS['db_type']; 996 $field = views_date_sql_field($field, $field_type, $set_offset); 997 998 // Note there is no space after FROM to avoid db_rewrite problems 999 // see http://drupal.org/node/79904. 1000 switch ($extract_type) { 1001 case('DATE'): 1002 return $field; 1003 case('YEAR'): 1004 return "EXTRACT(YEAR FROM($field))"; 1005 case('MONTH'): 1006 return "EXTRACT(MONTH FROM($field))"; 1007 case('DAY'): 1008 return "EXTRACT(DAY FROM($field))"; 1009 case('HOUR'): 1010 return "EXTRACT(HOUR FROM($field))"; 1011 case('MINUTE'): 1012 return "EXTRACT(MINUTE FROM($field))"; 1013 case('SECOND'): 1014 return "EXTRACT(SECOND FROM($field))"; 1015 case('WEEK'): // ISO week number for date 1016 switch ($db_type) { 1017 case('mysql'): 1018 case('mysqli'): 1019 // WEEK using arg 3 in mysql should return the same value as postgres EXTRACT 1020 return "WEEK($field, 3)"; 1021 case('pgsql'): 1022 return "EXTRACT(WEEK FROM($field))"; 1023 } 1024 case('DOW'): 1025 switch ($db_type) { 1026 case('mysql'): 1027 case('mysqli'): 1028 // mysql returns 1 for Sunday through 7 for Saturday 1029 // php date functions and postgres use 0 for Sunday and 6 for Saturday 1030 return "INTEGER(DAYOFWEEK($field) - 1)"; 1031 case('pgsql'): 1032 return "EXTRACT(DOW FROM($field))"; 1033 } 1034 case('DOY'): 1035 switch ($db_type) { 1036 case('mysql'): 1037 case('mysqli'): 1038 return "DAYOFYEAR($field)"; 1039 case('pgsql'): 1040 return "EXTRACT(DOY FROM($field))"; 1041 } 1042 } 1043} 1044 1045/** 1046 * Implementation of hook_views_handlers() to register all of the basic handlers 1047 * views uses. 1048 */ 1049function views_views_handlers() { 1050 return array( 1051 'info' => array( 1052 'path' => drupal_get_path('module', 'views') . '/handlers', 1053 ), 1054 'handlers' => array( 1055 // argument handlers 1056 'views_handler_argument' => array( 1057 'parent' => 'views_handler', 1058 ), 1059 'views_handler_argument_numeric' => array( 1060 'parent' => 'views_handler_argument', 1061 ), 1062 'views_handler_argument_formula' => array( 1063 'parent' => 'views_handler_argument', 1064 ), 1065 'views_handler_argument_date' => array( 1066 'parent' => 'views_handler_argument_formula', 1067 ), 1068 'views_handler_argument_string' => array( 1069 'parent' => 'views_handler_argument', 1070 ), 1071 'views_handler_argument_many_to_one' => array( 1072 'parent' => 'views_handler_argument', 1073 ), 1074 'views_handler_argument_null' => array( 1075 'parent' => 'views_handler_argument', 1076 ), 1077 'views_handler_argument_broken' => array( 1078 'parent' => 'views_handler_argument', 1079 ), 1080 1081 // field handlers 1082 'views_handler_field' => array( 1083 'parent' => 'views_handler', 1084 ), 1085 'views_handler_field_date' => array( 1086 'parent' => 'views_handler_field', 1087 ), 1088 'views_handler_field_boolean' => array( 1089 'parent' => 'views_handler_field', 1090 ), 1091 'views_handler_field_markup' => array( 1092 'parent' => 'views_handler_field', 1093 ), 1094 'views_handler_field_xss' => array( 1095 'parent' => 'views_handler_field', 1096 'file' => 'views_handler_field.inc', 1097 ), 1098 'views_handler_field_url' => array( 1099 'parent' => 'views_handler_field', 1100 ), 1101 'views_handler_field_file_size' => array( 1102 'parent' => 'views_handler_field', 1103 'file' => 'views_handler_field.inc', 1104 ), 1105 'views_handler_field_prerender_list' => array( 1106 'parent' => 'views_handler_field', 1107 ), 1108 'views_handler_field_numeric' => array( 1109 'parent' => 'views_handler_field', 1110 ), 1111 'views_handler_field_custom' => array( 1112 'parent' => 'views_handler_field', 1113 ), 1114 'views_handler_field_counter' => array( 1115 'parent' => 'views_handler_field', 1116 ), 1117 'views_handler_field_math' => array( 1118 'parent' => 'views_handler_field_numeric', 1119 ), 1120 'views_handler_field_broken' => array( 1121 'parent' => 'views_handler_field', 1122 ), 1123 1124 // filter handlers 1125 'views_handler_filter' => array( 1126 'parent' => 'views_handler', 1127 ), 1128 'views_handler_filter_equality' => array( 1129 'parent' => 'views_handler_filter', 1130 ), 1131 'views_handler_filter_string' => array( 1132 'parent' => 'views_handler_filter', 1133 ), 1134 'views_handler_filter_boolean_operator' => array( 1135 'parent' => 'views_handler_filter', 1136 ), 1137 'views_handler_filter_boolean_operator_string' => array( 1138 'parent' => 'views_handler_filter_boolean_operator', 1139 ), 1140 'views_handler_filter_in_operator' => array( 1141 'parent' => 'views_handler_filter', 1142 ), 1143 'views_handler_filter_numeric' => array( 1144 'parent' => 'views_handler_filter', 1145 ), 1146 'views_handler_filter_float' => array( 1147 'parent' => 'views_handler_filter_numeric', 1148 ), 1149 'views_handler_filter_date' => array( 1150 'parent' => 'views_handler_filter_numeric', 1151 ), 1152 'views_handler_filter_many_to_one' => array( 1153 'parent' => 'views_handler_filter_in_operator', 1154 ), 1155 'views_handler_filter_broken' => array( 1156 'parent' => 'views_handler_filter', 1157 ), 1158 1159 // relationship handlers 1160 'views_handler_relationship' => array( 1161 'parent' => 'views_handler', 1162 ), 1163 'views_handler_relationship_broken' => array( 1164 'parent' => 'views_handler_relationship', 1165 ), 1166 1167 1168 // sort handlers 1169 'views_handler_sort' => array( 1170 'parent' => 'views_handler', 1171 ), 1172 'views_handler_sort_formula' => array( 1173 'parent' => 'views_handler_sort', 1174 ), 1175 'views_handler_sort_date' => array( 1176 'parent' => 'views_handler_sort', 1177 ), 1178 'views_handler_sort_menu_hierarchy' => array( 1179 'parent' => 'views_handler_sort', 1180 ), 1181 'views_handler_sort_random' => array( 1182 'parent' => 'views_handler_sort', 1183 ), 1184 'views_handler_sort_broken' => array( 1185 'parent' => 'views_handler_sort', 1186 ), 1187 ), 1188 ); 1189} 1190 1191 1192/** 1193 * @} 1194 */ 1195 1196/** 1197 * @defgroup views_join_handlers Views' join handlers 1198 * @{ 1199 * Handlers to tell Views how to join tables together. 1200 1201 * Here is how you do complex joins: 1202 * 1203 * @code 1204 * class views_join_complex extends views_join { 1205 * // PHP 4 doesn't call constructors of the base class automatically from a 1206 * // constructor of a derived class. It is your responsibility to propagate 1207 * // the call to constructors upstream where appropriate. 1208 * function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') { 1209 * parent::construct($table, $left_table, $left_field, $field, $extra, $type); 1210 * } 1211 * 1212 * function join($table, &$query) { 1213 * $output = parent::join($table, $query); 1214 * $output .= "AND foo.bar = baz.boing"; 1215 * return $output; 1216 * } 1217 * } 1218 * @endcode 1219 */ 1220/** 1221 * A function class to represent a join and create the SQL necessary 1222 * to implement the join. 1223 * 1224 * This is the Delegation pattern. If we had PHP5 exclusively, we would 1225 * declare this an interface. 1226 * 1227 * Extensions of this class can be used to create more interesting joins. 1228 * 1229 * join definition 1230 * - table: table to join (right table) 1231 * - field: field to join on (right field) 1232 * - left_table: The table we join to 1233 * - left_field: The field we join to 1234 * - type: either LEFT (default) or INNER 1235 * - extra: Either a string that's directly added, or an array of items: 1236 * - - table: if not set, current table; if NULL, no table. This field can't 1237 * be set in the cached definition because it can't know aliases; this field 1238 * can only be used by realtime joins. 1239 * - - field: Field or formula 1240 * - - operator: defaults to = 1241 * - - value: Must be set. If an array, operator will be defaulted to IN. 1242 * - - numeric: If true, the value will not be surrounded in quotes. 1243 * - - raw: If you specify raw the value will be used as it is, so you can have field to field conditions. 1244 * - extra type: How all the extras will be combined. Either AND or OR. Defaults to AND. 1245 */ 1246class views_join { 1247 /** 1248 * Construct the views_join object. 1249 */ 1250 function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') { 1251 $this->extra_type = 'AND'; 1252 if (!empty($table)) { 1253 $this->table = $table; 1254 $this->left_table = $left_table; 1255 $this->left_field = $left_field; 1256 $this->field = $field; 1257 $this->extra = $extra; 1258 $this->type = strtoupper($type); 1259 } 1260 else if (!empty($this->definition)) { 1261 // if no arguments, construct from definition. 1262 // These four must exist or it will throw notices. 1263 $this->table = $this->definition['table']; 1264 $this->left_table = $this->definition['left_table']; 1265 $this->left_field = $this->definition['left_field']; 1266 $this->field = $this->definition['field']; 1267 if (!empty($this->definition['extra'])) { 1268 $this->extra = $this->definition['extra']; 1269 } 1270 if (!empty($this->definition['extra type'])) { 1271 $this->extra_type = strtoupper($this->definition['extra type']); 1272 } 1273 1274 $this->type = !empty($this->definition['type']) ? strtoupper($this->definition['type']) : 'LEFT'; 1275 } 1276 } 1277 1278 /** 1279 * Build the SQL for the join this object represents. 1280 */ 1281 function join($table, &$query) { 1282 if (empty($this->definition['table formula'])) { 1283 $right_table = "{" . $this->table . "}"; 1284 } 1285 else { 1286 $right_table = $this->definition['table formula']; 1287 } 1288 1289 if ($this->left_table) { 1290 $left = $query->get_table_info($this->left_table); 1291 $left_field = "$left[alias].$this->left_field"; 1292 } 1293 else { 1294 // This can be used if left_field is a formula or something. It should be used only *very* rarely. 1295 $left_field = $this->left_field; 1296 } 1297 1298 $output = " $this->type JOIN $right_table $table[alias] ON $left_field = $table[alias].$this->field"; 1299 1300 // Load query tokens replacements. 1301 $view = views_get_current_view(); 1302 $replacements = array(); 1303 if (!empty($view)) { 1304 $replacements = $view->substitutions(); 1305 } 1306 else { 1307 vpr('The current view is not set, maybe some code missed to execute $view->pre_execute()'); 1308 } 1309 1310 // Tack on the extra. 1311 if (isset($this->extra)) { 1312 if (is_array($this->extra)) { 1313 $extras = array(); 1314 foreach ($this->extra as $info) { 1315 $extra = ''; 1316 // Figure out the table name. Remember, only use aliases provided 1317 // if at all possible. 1318 $join_table = ''; 1319 if (!array_key_exists('table', $info)) { 1320 $join_table = $table['alias'] . '.'; 1321 } 1322 elseif (isset($info['table'])) { 1323 $join_table = $info['table'] . '.'; 1324 } 1325 1326 // Apply query token replacements. 1327 $info['value'] = str_replace(array_keys($replacements), $replacements, $info['value']); 1328 1329 // And now deal with the value and the operator. Set $q to 1330 // a single-quote for non-numeric values and the 1331 // empty-string for numeric values, then wrap all values in $q. 1332 if (empty($info['raw'])) { 1333 $raw_value = $this->db_safe($info['value'], $info); 1334 $q = (empty($info['numeric']) ? "'" : ''); 1335 } 1336 else { 1337 $raw_value = $info['value']; 1338 $q = ''; 1339 } 1340 1341 if (is_array($raw_value)) { 1342 $operator = !empty($info['operator']) ? $info['operator'] : 'IN'; 1343 // Transform from IN() notation to = notation if just one value. 1344 if (count($raw_value) == 1) { 1345 $value = $q . array_shift($raw_value) . $q; 1346 $operator = $operator == 'NOT IN' ? '!=' : '='; 1347 } 1348 else { 1349 $value = "($q" . implode("$q, $q", $raw_value) . "$q)"; 1350 } 1351 } 1352 else { 1353 $operator = !empty($info['operator']) ? $info['operator'] : '='; 1354 $value = "$q$raw_value$q"; 1355 } 1356 $extras[] = "$join_table$info[field] $operator $value"; 1357 } 1358 1359 if ($extras) { 1360 if (count($extras) == 1) { 1361 $output .= ' AND ' . array_shift($extras); 1362 } 1363 else { 1364 $output .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')'; 1365 } 1366 } 1367 } 1368 else if ($this->extra && is_string($this->extra)) { 1369 $output .= " AND ($this->extra)"; 1370 } 1371 } 1372 return $output; 1373 } 1374 1375 /** 1376 * Ensure that input is db safe. We only check strings and ints tho 1377 * so something that needs floats in their joins needs to do their 1378 * own type checking. 1379 */ 1380 function db_safe($input, $info) { 1381 if (is_array($input)) { 1382 $output = array(); 1383 foreach ($input as $value) { 1384 if (empty($info['numeric'])) { 1385 $output[] = db_escape_string($value); 1386 } 1387 else { 1388 $output[] = intval($value); 1389 } 1390 } 1391 } 1392 else if (empty($info['numeric'])) { 1393 $output = db_escape_string($input); 1394 } 1395 else { 1396 $output = intval($input); 1397 } 1398 1399 return $output; 1400 } 1401} 1402 1403/** 1404 * @} 1405 */ 1406 1407// Declare API compatibility on behalf of core modules: 1408 1409/** 1410 * Implementation of hook_views_api(). 1411 * 1412 * This one is used as the base to reduce errors when updating. 1413 */ 1414function views_views_api() { 1415 return array( 1416 'api' => 2, 1417 'path' => drupal_get_path('module', 'views') . '/modules', 1418 ); 1419} 1420 1421if (!function_exists('aggregator_views_api')) { 1422 function aggregator_views_api() { return views_views_api(); } 1423} 1424 1425if (!function_exists('book_views_api')) { 1426 function book_views_api() { return views_views_api(); } 1427} 1428 1429if (!function_exists('comment_views_api')) { 1430 function comment_views_api() { return views_views_api(); } 1431} 1432 1433if (!function_exists('locale_views_api')) { 1434 function locale_views_api() { return views_views_api(); } 1435} 1436 1437if (!function_exists('filter_views_api')) { 1438 function filter_views_api() { return views_views_api(); } 1439} 1440 1441if (!function_exists('node_views_api')) { 1442 function node_views_api() { return views_views_api(); } 1443} 1444 1445if (!function_exists('poll_views_api')) { 1446 function poll_views_api() { return views_views_api(); } 1447} 1448 1449if (!function_exists('profile_views_api')) { 1450 function profile_views_api() { return views_views_api(); } 1451} 1452 1453if (!function_exists('search_views_api')) { 1454 function search_views_api() { return views_views_api(); } 1455} 1456 1457if (!function_exists('statistics_views_api')) { 1458 function statistics_views_api() { return views_views_api(); } 1459} 1460 1461if (!function_exists('system_views_api')) { 1462 function system_views_api() { return views_views_api(); } 1463} 1464 1465if (!function_exists('taxonomy_views_api')) { 1466 function taxonomy_views_api() { return views_views_api(); } 1467} 1468 1469if (!function_exists('translation_views_api')) { 1470 function translation_views_api() { return views_views_api(); } 1471} 1472 1473if (!function_exists('upload_views_api')) { 1474 function upload_views_api() { return views_views_api(); } 1475} 1476 1477if (!function_exists('user_views_api')) { 1478 function user_views_api() { return views_views_api(); } 1479} 1480 1481if (!function_exists('contact_views_api')) { 1482 function contact_views_api() { return views_views_api(); } 1483} 1484