Project

General

Profile

Paste
Download (53.8 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / views / includes / handlers.inc @ d719f12f

1
<?php
2

    
3
/**
4
 * @file
5
 * Defines the various handler objects to help build and display views.
6
 */
7

    
8
/**
9
 * Instantiate and construct a new handler
10
 */
11
function _views_create_handler($definition, $type = 'handler', $handler_type = NULL) {
12
//  debug('Instantiating handler ' . $definition['handler']);
13
  if (empty($definition['handler'])) {
14
    vpr('_views_create_handler - type: @type - failed: handler has not been provided.',
15
      array('@type' => isset($handler_type) ? ( $type . '(handler type: ' . $handler_type . ')' ) : $type)
16
    );
17
    return;
18
  }
19

    
20
  // class_exists will automatically load the code file.
21
  if (!empty($definition['override handler']) &&
22
      !class_exists($definition['override handler'])) {
23
    vpr(
24
      '_views_create_handler - loading override handler @type failed: class @override_handler could not be loaded. ' .
25
      'Verify the class file has been registered in the corresponding .info-file (files[]).',
26
      array(
27
        '@type' => isset($handler_type) ? ( $type . '(handler type: ' . $handler_type . ')' ) : $type,
28
        '@override_handler' => $definition['override handler']
29
      )
30
    );
31
    return;
32
  }
33

    
34
  if (!class_exists($definition['handler'])) {
35
    vpr(
36
      '_views_create_handler - loading handler @type failed: class @handler could not be loaded. ' .
37
      'Verify the class file has been registered in the corresponding .info-file (files[]).',
38
      array(
39
        '@type' => isset($handler_type) ? ( $type . '(handler type: ' . $handler_type . ')' ) : $type,
40
        '@handler' => $definition['handler']
41
      )
42
    );
43
    return;
44
  }
45

    
46
   if (!empty($definition['override handler'])) {
47
     $handler = new $definition['override handler'];
48
   }
49
   else {
50
     $handler = new $definition['handler'];
51
   }
52

    
53
  $handler->set_definition($definition);
54
  if ($type == 'handler') {
55
    $handler->is_handler = TRUE;
56
    $handler->handler_type = $handler_type;
57
  }
58
  else {
59
    $handler->is_plugin = TRUE;
60
    $handler->plugin_type = $type;
61
    $handler->plugin_name = $definition['name'];
62
  }
63

    
64
  // let the handler have something like a constructor.
65
  $handler->construct();
66

    
67
  return $handler;
68
}
69

    
70
/**
71
 * Prepare a handler's data by checking defaults and such.
72
 */
73
function _views_prepare_handler($definition, $data, $field, $type) {
74
  foreach (array('group', 'title', 'title short', 'help', 'real field') as $key) {
75
    if (!isset($definition[$key])) {
76
      // First check the field level
77
      if (!empty($data[$field][$key])) {
78
        $definition[$key] = $data[$field][$key];
79
      }
80
      // Then if that doesn't work, check the table level
81
      elseif (!empty($data['table'][$key])) {
82
        $definition[$key] = $data['table'][$key];
83
      }
84
    }
85
  }
86

    
87
  return _views_create_handler($definition, 'handler', $type);
88
}
89

    
90
/**
91
 * Fetch a handler to join one table to a primary table from the data cache
92
 */
93
function views_get_table_join($table, $base_table) {
94
  $data = views_fetch_data($table);
95
  if (isset($data['table']['join'][$base_table])) {
96
    $h = $data['table']['join'][$base_table];
97
    if (!empty($h['handler']) && class_exists($h['handler'])) {
98
      $handler = new $h['handler'];
99
    }
100
    else {
101
      $handler = new views_join();
102
    }
103

    
104
    // Fill in some easy defaults
105
    $handler->definition = $h;
106
    if (empty($handler->definition['table'])) {
107
      $handler->definition['table'] = $table;
108
    }
109
    // If this is empty, it's a direct link.
110
    if (empty($handler->definition['left_table'])) {
111
      $handler->definition['left_table'] = $base_table;
112
    }
113

    
114
    if (isset($h['arguments'])) {
115
      call_user_func_array(array(&$handler, 'construct'), $h['arguments']);
116
    }
117
    else {
118
      $handler->construct();
119
    }
120

    
121
    return $handler;
122
  }
123

    
124
  // DEBUG -- identify missing handlers
125
  vpr("Missing join: @table @base_table", array('@table' => $table, '@base_table' => $base_table));
126
}
127

    
128
/**
129
 * Base handler, from which all the other handlers are derived.
130
 * It creates a common interface to create consistency amongst
131
 * handlers and data.
132
 *
133
 * This class would be abstract in PHP5, but PHP4 doesn't understand that.
134
 *
135
 * Definition terms:
136
 * - table: The actual table this uses; only specify if different from
137
 *          the table this is attached to.
138
 * - real field: The actual field this uses; only specify if different from
139
 *               the field this item is attached to.
140
 * - group: A text string representing the 'group' this item is attached to,
141
 *          for display in the UI. Examples: "Node", "Taxonomy", "Comment",
142
 *          "User", etc. This may be inherited from the parent definition or
143
 *          the 'table' definition.
144
 * - title: The title for this handler in the UI. This may be inherited from
145
 *          the parent definition or the 'table' definition.
146
 * - help: A more informative string to give to the user to explain what this
147
 *         field/handler is or does.
148
 * - access callback: If this field should have access control, this could
149
 *                    be a function to use. 'user_access' is a common
150
 *                    function to use here. If not specified, no access
151
 *                    control is provided.
152
 * - access arguments: An array of arguments for the access callback.
153
 */
154
class views_handler extends views_object {
155
  /**
156
   * The top object of a view.
157
   *
158
   * @var view
159
   */
160
  var $view = NULL;
161

    
162
  /**
163
   * Where the $query object will reside:
164
   *
165
   * @var views_plugin_query
166
   */
167
  var $query = NULL;
168

    
169
  /**
170
   * The type of the handler, for example filter/footer/field.
171
   */
172
  var $handler_type = NULL;
173

    
174
  /**
175
   * The alias of the table of this handler which is used in the query.
176
   */
177
  public $table_alias;
178

    
179
  /**
180
   * The actual field in the database table, maybe different
181
   * on other kind of query plugins/special handlers.
182
   */
183
  var $real_field;
184

    
185
  /**
186
   * The relationship used for this field.
187
   */
188
  var $relationship = NULL;
189

    
190
  /**
191
   * init the handler with necessary data.
192
   * @param $view
193
   *   The $view object this handler is attached to.
194
   * @param $options
195
   *   The item from the database; the actual contents of this will vary
196
   *   based upon the type of handler.
197
   */
198
  function init(&$view, &$options) {
199
    $this->view = &$view;
200
    $display_id = $this->view->current_display;
201
    // Check to see if this handler type is defaulted. Note that
202
    // we have to do a lookup because the type is singular but the
203
    // option is stored as the plural.
204

    
205
    // If the 'moved to' keyword moved our handler, let's fix that now.
206
    if (isset($this->actual_table)) {
207
      $options['table'] = $this->actual_table;
208
    }
209

    
210
    if (isset($this->actual_field)) {
211
      $options['field'] = $this->actual_field;
212
    }
213

    
214
    $types = views_object_types();
215
    $plural = $this->handler_type;
216
    if (isset($types[$this->handler_type]['plural'])) {
217
      $plural = $types[$this->handler_type]['plural'];
218
    }
219
    if ($this->view->display_handler->is_defaulted($plural)) {
220
      $display_id = 'default';
221
    }
222

    
223
    $this->localization_keys = array(
224
      $display_id,
225
      $this->handler_type,
226
      $options['table'],
227
      $options['id']
228
    );
229

    
230
    $this->unpack_options($this->options, $options);
231

    
232
    // This exist on most handlers, but not all. So they are still optional.
233
    if (isset($options['table'])) {
234
      $this->table = $options['table'];
235
    }
236

    
237
    if (isset($this->definition['real field'])) {
238
      $this->real_field = $this->definition['real field'];
239
    }
240

    
241
    if (isset($this->definition['field'])) {
242
      $this->real_field = $this->definition['field'];
243
    }
244

    
245
    if (isset($options['field'])) {
246
      $this->field = $options['field'];
247
      if (!isset($this->real_field)) {
248
        $this->real_field = $options['field'];
249
      }
250
    }
251

    
252
    $this->query = &$view->query;
253
  }
254

    
255
  function option_definition() {
256
    $options = parent::option_definition();
257

    
258
    $options['id'] = array('default' => '');
259
    $options['table'] = array('default' => '');
260
    $options['field'] = array('default' => '');
261
    $options['relationship'] = array('default' => 'none');
262
    $options['group_type'] = array('default' => 'group');
263
    $options['ui_name'] = array('default' => '');
264

    
265
    return $options;
266
  }
267

    
268
  /**
269
   * Return a string representing this handler's name in the UI.
270
   */
271
  function ui_name($short = FALSE) {
272
    if (!empty($this->options['ui_name'])) {
273
      $title = check_plain($this->options['ui_name']);
274
      return $title;
275
    }
276
    $title = ($short && isset($this->definition['title short'])) ? $this->definition['title short'] : $this->definition['title'];
277
    return t('!group: !title', array('!group' => $this->definition['group'], '!title' => $title));
278
  }
279

    
280
  /**
281
   * Shortcut to get a handler's raw field value.
282
   *
283
   * This should be overridden for handlers with formulae or other
284
   * non-standard fields. Because this takes an argument, fields
285
   * overriding this can just call return parent::get_field($formula)
286
   */
287
  function get_field($field = NULL) {
288
    if (!isset($field)) {
289
      if (!empty($this->formula)) {
290
        $field = $this->get_formula();
291
      }
292
      else {
293
        $field = $this->table_alias . '.' . $this->real_field;
294
      }
295
    }
296

    
297
    // If grouping, check to see if the aggregation method needs to modify the field.
298
    if ($this->view->display_handler->use_group_by()) {
299
      $this->view->init_query();
300
      if ($this->query) {
301
        $info = $this->query->get_aggregation_info();
302
        if (!empty($info[$this->options['group_type']]['method']) && function_exists($info[$this->options['group_type']]['method'])) {
303
          return $info[$this->options['group_type']]['method']($this->options['group_type'], $field);
304
        }
305
      }
306
    }
307

    
308
    return $field;
309
  }
310

    
311
  /**
312
   * Sanitize the value for output.
313
   *
314
   * @param $value
315
   *   The value being rendered.
316
   * @param $type
317
   *   The type of sanitization needed. If not provided, check_plain() is used.
318
   *
319
   * @return string
320
   *   Returns the safe value.
321
   */
322
  function sanitize_value($value, $type = NULL) {
323
    switch ($type) {
324
      case 'xss':
325
        $value = filter_xss($value);
326
        break;
327
      case 'xss_admin':
328
        $value = filter_xss_admin($value);
329
        break;
330
      case 'url':
331
        $value = check_url($value);
332
        break;
333
      default:
334
        $value = check_plain($value);
335
        break;
336
    }
337
    return $value;
338
  }
339

    
340
  /**
341
   * Transform a string by a certain method.
342
   *
343
   * @param $string
344
   *    The input you want to transform.
345
   * @param $option
346
   *    How do you want to transform it, possible values:
347
   *      - upper: Uppercase the string.
348
   *      - lower: lowercase the string.
349
   *      - ucfirst: Make the first char uppercase.
350
   *      - ucwords: Make each word in the string uppercase.
351
   *
352
   * @return string
353
   *    The transformed string.
354
   */
355
  function case_transform($string, $option) {
356
    global $multibyte;
357

    
358
    switch ($option) {
359
      default:
360
        return $string;
361
      case 'upper':
362
        return drupal_strtoupper($string);
363
      case 'lower':
364
        return drupal_strtolower($string);
365
      case 'ucfirst':
366
        return drupal_strtoupper(drupal_substr($string, 0, 1)) . drupal_substr($string, 1);
367
      case 'ucwords':
368
        if ($multibyte == UNICODE_MULTIBYTE) {
369
          return mb_convert_case($string, MB_CASE_TITLE);
370
        }
371
        else {
372
          return ucwords($string);
373
        }
374
    }
375
  }
376

    
377
  /**
378
   * Validate the options form.
379
   */
380
  function options_validate(&$form, &$form_state) { }
381

    
382
  /**
383
   * Build the options form.
384
   */
385
  function options_form(&$form, &$form_state) {
386
    // Some form elements belong in a fieldset for presentation, but can't
387
    // be moved into one because of the form_state['values'] hierarchy. Those
388
    // elements can add a #fieldset => 'fieldset_name' property, and they'll
389
    // be moved to their fieldset during pre_render.
390
    $form['#pre_render'][] = 'views_ui_pre_render_add_fieldset_markup';
391

    
392
    $form['ui_name'] = array(
393
      '#type' => 'textfield',
394
      '#title' => t('Administrative title'),
395
      '#description' => t('This title will be displayed on the views edit page instead of the default one. This might be useful if you have the same item twice.'),
396
      '#default_value' => $this->options['ui_name'],
397
      '#fieldset' => 'more',
398
    );
399

    
400
    // This form is long and messy enough that the "Administrative title" option
401
    // belongs in a "more options" fieldset at the bottom of the form.
402
    $form['more'] = array(
403
      '#type' => 'fieldset',
404
      '#title' => t('More'),
405
      '#collapsible' => TRUE,
406
      '#collapsed' => TRUE,
407
      '#weight' => 150,
408
    );
409
    // Allow to alter the default values brought into the form.
410
    drupal_alter('views_handler_options', $this->options, $view);
411
  }
412

    
413
  /**
414
   * Perform any necessary changes to the form values prior to storage.
415
   * There is no need for this function to actually store the data.
416
   */
417
  function options_submit(&$form, &$form_state) { }
418

    
419
  /**
420
   * Provides the handler some groupby.
421
   */
422
  function use_group_by() {
423
    return TRUE;
424
  }
425
  /**
426
   * Provide a form for aggregation settings.
427
   */
428
  function groupby_form(&$form, &$form_state) {
429
    $view = &$form_state['view'];
430
    $display_id = $form_state['display_id'];
431
    $types = views_object_types();
432
    $type = $form_state['type'];
433
    $id = $form_state['id'];
434

    
435
    $form['#title'] = check_plain($view->display[$display_id]->display_title) . ': ';
436
    $form['#title'] .= t('Configure aggregation settings for @type %item', array('@type' => $types[$type]['lstitle'], '%item' => $this->ui_name()));
437

    
438
    $form['#section'] = $display_id . '-' . $type . '-' . $id;
439

    
440
    $view->init_query();
441
    $info = $view->query->get_aggregation_info();
442
    foreach ($info as $id => $aggregate) {
443
      $group_types[$id] = $aggregate['title'];
444
    }
445

    
446
    $form['group_type'] = array(
447
      '#type' => 'select',
448
      '#title' => t('Aggregation type'),
449
      '#default_value' => $this->options['group_type'],
450
      '#description' => t('Select the aggregation function to use on this field.'),
451
      '#options' => $group_types,
452
    );
453
  }
454

    
455
  /**
456
   * Perform any necessary changes to the form values prior to storage.
457
   * There is no need for this function to actually store the data.
458
   */
459
  function groupby_form_submit(&$form, &$form_state) {
460
    $item =& $form_state['handler']->options;
461

    
462
    $item['group_type'] = $form_state['values']['options']['group_type'];
463
  }
464

    
465
  /**
466
   * If a handler has 'extra options' it will get a little settings widget and
467
   * another form called extra_options.
468
   */
469
  function has_extra_options() { return FALSE; }
470

    
471
  /**
472
   * Provide defaults for the handler.
473
   */
474
  function extra_options(&$option) { }
475

    
476
  /**
477
   * Provide a form for setting options.
478
   */
479
  function extra_options_form(&$form, &$form_state) { }
480

    
481
  /**
482
   * Validate the options form.
483
   */
484
  function extra_options_validate($form, &$form_state) { }
485

    
486
  /**
487
   * Perform any necessary changes to the form values prior to storage.
488
   * There is no need for this function to actually store the data.
489
   */
490
  function extra_options_submit($form, &$form_state) { }
491

    
492
  /**
493
   * Determine if a handler can be exposed.
494
   */
495
  function can_expose() { return FALSE; }
496

    
497
  /**
498
   * Set new exposed option defaults when exposed setting is flipped
499
   * on.
500
   */
501
  function expose_options() { }
502

    
503
  /**
504
   * Get information about the exposed form for the form renderer.
505
   */
506
  function exposed_info() { }
507

    
508
  /**
509
   * Render our chunk of the exposed handler form when selecting
510
   */
511
  function exposed_form(&$form, &$form_state) { }
512

    
513
  /**
514
   * Validate the exposed handler form
515
   */
516
  function exposed_validate(&$form, &$form_state) { }
517

    
518
  /**
519
   * Submit the exposed handler form
520
   */
521
  function exposed_submit(&$form, &$form_state) { }
522

    
523
  /**
524
   * Form for exposed handler options.
525
   */
526
  function expose_form(&$form, &$form_state) { }
527

    
528
  /**
529
   * Validate the options form.
530
   */
531
  function expose_validate($form, &$form_state) { }
532

    
533
  /**
534
   * Perform any necessary changes to the form exposes prior to storage.
535
   * There is no need for this function to actually store the data.
536
   */
537
  function expose_submit($form, &$form_state) { }
538

    
539
  /**
540
   * Shortcut to display the expose/hide button.
541
   */
542
  function show_expose_button(&$form, &$form_state) { }
543

    
544
  /**
545
   * Shortcut to display the exposed options form.
546
   */
547
  function show_expose_form(&$form, &$form_state) {
548
    if (empty($this->options['exposed'])) {
549
      return;
550
    }
551

    
552
    $this->expose_form($form, $form_state);
553

    
554
    // When we click the expose button, we add new gadgets to the form but they
555
    // have no data in $_POST so their defaults get wiped out. This prevents
556
    // these defaults from getting wiped out. This setting will only be TRUE
557
    // during a 2nd pass rerender.
558
    if (!empty($form_state['force_expose_options'])) {
559
      foreach (element_children($form['expose']) as $id) {
560
        if (isset($form['expose'][$id]['#default_value']) && !isset($form['expose'][$id]['#value'])) {
561
          $form['expose'][$id]['#value'] = $form['expose'][$id]['#default_value'];
562
        }
563
      }
564
    }
565
  }
566

    
567
  /**
568
   * Check whether current user has access to this handler.
569
   *
570
   * @return boolean
571
   */
572
  function access() {
573
    if (isset($this->definition['access callback']) && function_exists($this->definition['access callback'])) {
574
      if (isset($this->definition['access arguments']) && is_array($this->definition['access arguments'])) {
575
        return call_user_func_array($this->definition['access callback'], $this->definition['access arguments']);
576
      }
577
      return $this->definition['access callback']();
578
    }
579

    
580
    return TRUE;
581
  }
582

    
583
  /**
584
   * Run before the view is built.
585
   *
586
   * This gives all the handlers some time to set up before any handler has
587
   * been fully run.
588
   */
589
  function pre_query() { }
590

    
591
  /**
592
   * Run after the view is executed, before the result is cached.
593
   *
594
   * This gives all the handlers some time to modify values. This is primarily
595
   * used so that handlers that pull up secondary data can put it in the
596
   * $values so that the raw data can be utilized externally.
597
   */
598
  function post_execute(&$values) { }
599

    
600
  /**
601
   * Provides a unique placeholders for handlers.
602
   */
603
  function placeholder() {
604
    return $this->query->placeholder($this->options['table'] . '_' . $this->options['field']);
605
  }
606

    
607
  /**
608
   * Called just prior to query(), this lets a handler set up any relationship
609
   * it needs.
610
   */
611
  function set_relationship() {
612
    // Ensure this gets set to something.
613
    $this->relationship = NULL;
614

    
615
    // Don't process non-existant relationships.
616
    if (empty($this->options['relationship']) || $this->options['relationship'] == 'none') {
617
      return;
618
    }
619

    
620
    $relationship = $this->options['relationship'];
621

    
622
    // Ignore missing/broken relationships.
623
    if (empty($this->view->relationship[$relationship])) {
624
      return;
625
    }
626

    
627
    // Check to see if the relationship has already processed. If not, then we
628
    // cannot process it.
629
    if (empty($this->view->relationship[$relationship]->alias)) {
630
      return;
631
    }
632

    
633
    // Finally!
634
    $this->relationship = $this->view->relationship[$relationship]->alias;
635
  }
636

    
637
  /**
638
   * Ensure the main table for this handler is in the query. This is used
639
   * a lot.
640
   */
641
  function ensure_my_table() {
642
    if (!isset($this->table_alias)) {
643
      if (!method_exists($this->query, 'ensure_table')) {
644
        vpr(t('Ensure my table called but query has no ensure_table method.'));
645
        return;
646
      }
647
      $this->table_alias = $this->query->ensure_table($this->table, $this->relationship);
648
    }
649
    return $this->table_alias;
650
  }
651

    
652
  /**
653
   * Provide text for the administrative summary
654
   */
655
  function admin_summary() { }
656

    
657
  /**
658
   * Determine if the argument needs a style plugin.
659
   *
660
   * @return TRUE/FALSE
661
   */
662
  function needs_style_plugin() { return FALSE; }
663

    
664
  /**
665
   * Determine if this item is 'exposed', meaning it provides form elements
666
   * to let users modify the view.
667
   *
668
   * @return TRUE/FALSE
669
   */
670
  function is_exposed() {
671
    return !empty($this->options['exposed']);
672
  }
673

    
674
  /**
675
   * Returns TRUE if the exposed filter works like a grouped filter.
676
   */
677
  function is_a_group() { return FALSE; }
678

    
679
  /**
680
   * Define if the exposed input has to be submitted multiple times.
681
   * This is TRUE when exposed filters grouped are using checkboxes as
682
   * widgets.
683
   */
684
  function multiple_exposed_input() { return FALSE; }
685

    
686
  /**
687
   * Take input from exposed handlers and assign to this handler, if necessary.
688
   */
689
  function accept_exposed_input($input) { return TRUE; }
690

    
691
  /**
692
   * If set to remember exposed input in the session, store it there.
693
   */
694
  function store_exposed_input($input, $status) { return TRUE; }
695

    
696
  /**
697
   * Get the join object that should be used for this handler.
698
   *
699
   * This method isn't used a great deal, but it's very handy for easily
700
   * getting the join if it is necessary to make some changes to it, such
701
   * as adding an 'extra'.
702
   */
703
  function get_join() {
704
    // get the join from this table that links back to the base table.
705
    // Determine the primary table to seek
706
    if (empty($this->query->relationships[$this->relationship])) {
707
      $base_table = $this->query->base_table;
708
    }
709
    else {
710
      $base_table = $this->query->relationships[$this->relationship]['base'];
711
    }
712

    
713
    $join = views_get_table_join($this->table, $base_table);
714
    if ($join) {
715
      return clone $join;
716
    }
717
  }
718

    
719
  /**
720
   * Validates the handler against the complete View.
721
   *
722
   * This is called when the complete View is being validated. For validating
723
   * the handler options form use options_validate().
724
   *
725
   * @see views_handler::options_validate()
726
   *
727
   * @return
728
   *   Empty array if the handler is valid; an array of error strings if it is not.
729
   */
730
  function validate() { return array(); }
731

    
732
  /**
733
   * Determine if the handler is considered 'broken', meaning it's a
734
   * a placeholder used when a handler can't be found.
735
   */
736
  function broken() { }
737
}
738

    
739
/**
740
 * This many to one helper object is used on both arguments and filters.
741
 *
742
 * @todo This requires extensive documentation on how this class is to
743
 * be used. For now, look at the arguments and filters that use it. Lots
744
 * of stuff is just pass-through but there are definitely some interesting
745
 * areas where they interact.
746
 *
747
 * Any handler that uses this can have the following possibly additional
748
 * definition terms:
749
 * - numeric: If true, treat this field as numeric, using %d instead of %s in
750
 *            queries.
751
 *
752
 */
753
class views_many_to_one_helper {
754
  /**
755
   * Contains possible existing placeholders used by the query.
756
   *
757
   * @var array
758
   */
759
  public $placeholders = array();
760

    
761
  function __construct(&$handler) {
762
    $this->handler = &$handler;
763
  }
764

    
765
  static function option_definition(&$options) {
766
    $options['reduce_duplicates'] = array('default' => FALSE, 'bool' => TRUE);
767
  }
768

    
769
  function options_form(&$form, &$form_state) {
770
    $form['reduce_duplicates'] = array(
771
      '#type' => 'checkbox',
772
      '#title' => t('Reduce duplicates'),
773
      '#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.'),
774
      '#default_value' => !empty($this->handler->options['reduce_duplicates']),
775
      '#weight' => 4,
776
    );
777
  }
778

    
779
  /**
780
   * Sometimes the handler might want us to use some kind of formula, so give
781
   * it that option. If it wants us to do this, it must set $helper->formula = TRUE
782
   * and implement handler->get_formula();
783
   */
784
  function get_field() {
785
    if (!empty($this->formula)) {
786
      return $this->handler->get_formula();
787
    }
788
    else {
789
      return $this->handler->table_alias . '.' . $this->handler->real_field;
790
    }
791
  }
792

    
793
  /**
794
   * Add a table to the query.
795
   *
796
   * This is an advanced concept; not only does it add a new instance of the table,
797
   * but it follows the relationship path all the way down to the relationship
798
   * link point and adds *that* as a new relationship and then adds the table to
799
   * the relationship, if necessary.
800
   */
801
  function add_table($join = NULL, $alias = NULL) {
802
    // This is used for lookups in the many_to_one table.
803
    $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
804

    
805
    if (empty($join)) {
806
      $join = $this->get_join();
807
    }
808

    
809
    // See if there's a chain between us and the base relationship. If so, we need
810
    // to create a new relationship to use.
811
    $relationship = $this->handler->relationship;
812

    
813
    // Determine the primary table to seek
814
    if (empty($this->handler->query->relationships[$relationship])) {
815
      $base_table = $this->handler->query->base_table;
816
    }
817
    else {
818
      $base_table = $this->handler->query->relationships[$relationship]['base'];
819
    }
820

    
821
    // Cycle through the joins. This isn't as error-safe as the normal
822
    // ensure_path logic. Perhaps it should be.
823
    $r_join = clone $join;
824
    while ($r_join->left_table != $base_table) {
825
      $r_join = views_get_table_join($r_join->left_table, $base_table);
826
    }
827
    // If we found that there are tables in between, add the relationship.
828
    if ($r_join->table != $join->table) {
829
      $relationship = $this->handler->query->add_relationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship);
830
    }
831

    
832
    // And now add our table, using the new relationship if one was used.
833
    $alias = $this->handler->query->add_table($this->handler->table, $relationship, $join, $alias);
834

    
835
    // Store what values are used by this table chain so that other chains can
836
    // automatically discard those values.
837
    if (empty($this->handler->view->many_to_one_tables[$field])) {
838
      $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
839
    }
840
    else {
841
      $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value);
842
    }
843

    
844
    return $alias;
845
  }
846

    
847
  function get_join() {
848
    return $this->handler->get_join();
849
  }
850

    
851
  /**
852
   * Provide the proper join for summary queries. This is important in part because
853
   * it will cooperate with other arguments if possible.
854
   */
855
  function summary_join() {
856
    $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
857
    $join = $this->get_join();
858

    
859
    // shortcuts
860
    $options = $this->handler->options;
861
    $view = &$this->handler->view;
862
    $query = &$this->handler->query;
863

    
864
    if (!empty($options['require_value'])) {
865
      $join->type = 'INNER';
866
    }
867

    
868
    if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) {
869
      return $query->ensure_table($this->handler->table, $this->handler->relationship, $join);
870
    }
871
    else {
872
      if (!empty($view->many_to_one_tables[$field])) {
873
        foreach ($view->many_to_one_tables[$field] as $value) {
874
          $join->extra = array(
875
            array(
876
              'field' => $this->handler->real_field,
877
              'operator' => '!=',
878
              'value' => $value,
879
              'numeric' => !empty($this->definition['numeric']),
880
            ),
881
          );
882
        }
883
      }
884
      return $this->add_table($join);
885
    }
886
  }
887

    
888
  /**
889
   * Override ensure_my_table so we can control how this joins in.
890
   * The operator actually has influence over joining.
891
   */
892
  function ensure_my_table() {
893
    if (!isset($this->handler->table_alias)) {
894
      // Case 1: Operator is an 'or' and we're not reducing duplicates.
895
      // We hence get the absolute simplest:
896
      $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
897
      if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) {
898
        if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) {
899
          // query optimization, INNER joins are slightly faster, so use them
900
          // when we know we can.
901
          $join = $this->get_join();
902
          if (isset($join)) {
903
            $join->type = 'INNER';
904
          }
905
          $this->handler->table_alias = $this->handler->query->ensure_table($this->handler->table, $this->handler->relationship, $join);
906
          $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
907
        }
908
        else {
909
          $join = $this->get_join();
910
          $join->type = 'LEFT';
911
          if (!empty($this->handler->view->many_to_one_tables[$field])) {
912
            foreach ($this->handler->view->many_to_one_tables[$field] as $value) {
913
              $join->extra = array(
914
                array(
915
                  'field' => $this->handler->real_field,
916
                  'operator' => '!=',
917
                  'value' => $value,
918
                  'numeric' => !empty($this->handler->definition['numeric']),
919
                ),
920
              );
921
            }
922
          }
923

    
924
          $this->handler->table_alias = $this->add_table($join);
925
        }
926

    
927
        return $this->handler->table_alias;
928
      }
929

    
930
      // Case 2: it's an 'and' or an 'or'.
931
      // We do one join per selected value.
932
      if ($this->handler->operator != 'not') {
933
        // Clone the join for each table:
934
        $this->handler->table_aliases = array();
935
        foreach ($this->handler->value as $value) {
936
          $join = $this->get_join();
937
          if ($this->handler->operator == 'and') {
938
            $join->type = 'INNER';
939
          }
940
          $join->extra = array(
941
            array(
942
              'field' => $this->handler->real_field,
943
              'value' => $value,
944
              'numeric' => !empty($this->handler->definition['numeric']),
945
            ),
946
          );
947

    
948
          // The table alias needs to be unique to this value across the
949
          // multiple times the filter or argument is called by the view.
950
          if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
951
            if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
952
              $this->handler->view->many_to_one_count[$this->handler->table] = 0;
953
            }
954
            $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
955
          }
956
          $alias = $this->handler->table_aliases[$value] = $this->add_table($join, $this->handler->view->many_to_one_aliases[$field][$value]);
957

    
958
          // and set table_alias to the first of these.
959
          if (empty($this->handler->table_alias)) {
960
            $this->handler->table_alias = $alias;
961
          }
962
        }
963
      }
964
      // Case 3: it's a 'not'.
965
      // We just do one join. We'll add a where clause during
966
      // the query phase to ensure that $table.$field IS NULL.
967
      else {
968
        $join = $this->get_join();
969
        $join->type = 'LEFT';
970
        $join->extra = array();
971
        $join->extra_type = 'OR';
972
        foreach ($this->handler->value as $value) {
973
          $join->extra[] = array(
974
            'field' => $this->handler->real_field,
975
            'value' => $value,
976
            'numeric' => !empty($this->handler->definition['numeric']),
977
          );
978
        }
979

    
980
        $this->handler->table_alias = $this->add_table($join);
981
      }
982
    }
983
    return $this->handler->table_alias;
984
  }
985

    
986
  /**
987
   * Provides a unique placeholders for handlers.
988
   */
989
  function placeholder() {
990
    return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
991
  }
992

    
993
  function add_filter() {
994
    if (empty($this->handler->value)) {
995
      return;
996
    }
997
    $this->handler->ensure_my_table();
998

    
999
    // Shorten some variables:
1000
    $field = $this->get_field();
1001
    $options = $this->handler->options;
1002
    $operator = $this->handler->operator;
1003
    $formula = !empty($this->formula);
1004
    $value = $this->handler->value;
1005
    if (empty($options['group'])) {
1006
      $options['group'] = 0;
1007
    }
1008

    
1009
    // add_condition determines whether a single expression is enough(FALSE) or the
1010
    // conditions should be added via an db_or()/db_and() (TRUE).
1011
    $add_condition = TRUE;
1012
    if ($operator == 'not') {
1013
      $value = NULL;
1014
      $operator = 'IS NULL';
1015
      $add_condition = FALSE;
1016
    }
1017
    elseif ($operator == 'or' && empty($options['reduce_duplicates'])) {
1018
      if (count($value) > 1) {
1019
        $operator = 'IN';
1020
      }
1021
      else {
1022
        $value = is_array($value) ? array_pop($value) : $value;
1023
        $operator = '=';
1024
      }
1025
      $add_condition = FALSE;
1026
    }
1027

    
1028
    if (!$add_condition) {
1029
      if ($formula) {
1030
        $placeholder = $this->placeholder();
1031
        if ($operator == 'IN') {
1032
          $operator = "$operator IN($placeholder)";
1033
        }
1034
        else {
1035
          $operator = "$operator $placeholder";
1036
        }
1037
        $placeholders = array(
1038
          $placeholder => $value,
1039
        ) + $this->placeholders;
1040
        $this->handler->query->add_where_expression($options['group'], "$field $operator", $placeholders);
1041
      }
1042
      else {
1043
        $this->handler->query->add_where($options['group'], $field, $value, $operator);
1044
      }
1045
    }
1046

    
1047
    if ($add_condition) {
1048
      $field = $this->handler->real_field;
1049
      $clause = $operator == 'or' ? db_or() : db_and();
1050
      foreach ($this->handler->table_aliases as $value => $alias) {
1051
        $clause->condition("$alias.$field", $value);
1052
      }
1053

    
1054
      // implode on either AND or OR.
1055
      $this->handler->query->add_where($options['group'], $clause);
1056
    }
1057
  }
1058
}
1059

    
1060
/**
1061
 * Break x,y,z and x+y+z into an array. Works for strings.
1062
 *
1063
 * @param $str
1064
 *   The string to parse.
1065
 * @param $object
1066
 *   The object to use as a base. If not specified one will
1067
 *   be created.
1068
 *
1069
 * @return $object
1070
 *   An object containing
1071
 *   - operator: Either 'and' or 'or'
1072
 *   - value: An array of numeric values.
1073
 */
1074
function views_break_phrase_string($str, &$handler = NULL) {
1075
  if (!$handler) {
1076
    $handler = new stdClass();
1077
  }
1078

    
1079
  // Set up defaults:
1080
  if (!isset($handler->value)) {
1081
    $handler->value = array();
1082
  }
1083

    
1084
  if (!isset($handler->operator)) {
1085
    $handler->operator = 'or';
1086
  }
1087

    
1088
  if ($str == '') {
1089
    return $handler;
1090
  }
1091

    
1092
  // Determine if the string has 'or' operators (plus signs) or 'and' operators
1093
  // (commas) and split the string accordingly. If we have an 'and' operator,
1094
  // spaces are treated as part of the word being split, but otherwise they are
1095
  // treated the same as a plus sign.
1096
  $or_wildcard = '[^\s+,]';
1097
  $and_wildcard = '[^+,]';
1098
  if (preg_match("/^({$or_wildcard}+[+ ])+{$or_wildcard}+$/", $str)) {
1099
    $handler->operator = 'or';
1100
    $handler->value = preg_split('/[+ ]/', $str);
1101
  }
1102
  elseif (preg_match("/^({$and_wildcard}+,)*{$and_wildcard}+$/", $str)) {
1103
    $handler->operator = 'and';
1104
    $handler->value = explode(',', $str);
1105
  }
1106

    
1107
  // Keep an 'error' value if invalid strings were given.
1108
  if (!empty($str) && (empty($handler->value) || !is_array($handler->value))) {
1109
    $handler->value = array(-1);
1110
    return $handler;
1111
  }
1112

    
1113
  // Doubly ensure that all values are strings only.
1114
  foreach ($handler->value as $id => $value) {
1115
    $handler->value[$id] = (string) $value;
1116
  }
1117

    
1118
  return $handler;
1119
}
1120

    
1121
/**
1122
 * Break x,y,z and x+y+z into an array. Numeric only.
1123
 *
1124
 * @param $str
1125
 *   The string to parse.
1126
 * @param $handler
1127
 *   The handler object to use as a base. If not specified one will
1128
 *   be created.
1129
 *
1130
 * @return $handler
1131
 *   The new handler object.
1132
 */
1133
function views_break_phrase($str, &$handler = NULL) {
1134
  if (!$handler) {
1135
    $handler = new stdClass();
1136
  }
1137

    
1138
  // Set up defaults:
1139

    
1140
  if (!isset($handler->value)) {
1141
    $handler->value = array();
1142
  }
1143

    
1144
  if (!isset($handler->operator)) {
1145
    $handler->operator = 'or';
1146
  }
1147

    
1148
  if (empty($str)) {
1149
    return $handler;
1150
  }
1151

    
1152
  if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) {
1153
    // The '+' character in a query string may be parsed as ' '.
1154
    $handler->operator = 'or';
1155
    $handler->value = preg_split('/[+ ]/', $str);
1156
  }
1157
  elseif (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) {
1158
    $handler->operator = 'and';
1159
    $handler->value = explode(',', $str);
1160
  }
1161

    
1162
  // Keep an 'error' value if invalid strings were given.
1163
  if (!empty($str) && (empty($handler->value) || !is_array($handler->value))) {
1164
    $handler->value = array(-1);
1165
    return $handler;
1166
  }
1167

    
1168
  // Doubly ensure that all values are numeric only.
1169
  foreach ($handler->value as $id => $value) {
1170
    $handler->value[$id] = intval($value);
1171
  }
1172

    
1173
  return $handler;
1174
}
1175

    
1176
// --------------------------------------------------------------------------
1177
// Date helper functions
1178

    
1179
/**
1180
 * Figure out what timezone we're in; needed for some date manipulations.
1181
 */
1182
function views_get_timezone() {
1183
  global $user;
1184
  if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
1185
    $timezone = $user->timezone;
1186
  }
1187
  else {
1188
    $timezone = variable_get('date_default_timezone', 0);
1189
  }
1190

    
1191
  // set up the database timezone
1192
  $db_type = Database::getConnection()->databaseType();
1193
  if (in_array($db_type, array('mysql', 'pgsql'))) {
1194
    $offset = '+00:00';
1195
    static $already_set = FALSE;
1196
    if (!$already_set) {
1197
      if ($db_type == 'pgsql') {
1198
        db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
1199
      }
1200
      elseif ($db_type == 'mysql') {
1201
        db_query("SET @@session.time_zone = '$offset'");
1202
      }
1203

    
1204
      $already_set = true;
1205
    }
1206
  }
1207

    
1208
  return $timezone;
1209
}
1210

    
1211
/**
1212
 * Helper function to create cross-database SQL dates.
1213
 *
1214
 * @param $field
1215
 *   The real table and field name, like 'tablename.fieldname'.
1216
 * @param $field_type
1217
 *  The type of date field, 'int' or 'datetime'.
1218
 * @param $set_offset
1219
 *   The name of a field that holds the timezone offset or a fixed timezone
1220
 *   offset value. If not provided, the normal Drupal timezone handling
1221
 *   will be used, i.e. $set_offset = 0 will make no timezone adjustment.
1222
 * @return
1223
 *   An appropriate SQL string for the db type and field type.
1224
 */
1225
function views_date_sql_field($field, $field_type = 'int', $set_offset = NULL) {
1226
  $db_type = Database::getConnection()->databaseType();
1227
  $offset = $set_offset !== NULL ? $set_offset : views_get_timezone();
1228
  if (isset($offset) && !is_numeric($offset)) {
1229
    $dtz = new DateTimeZone($offset);
1230
    $dt = new DateTime("now", $dtz);
1231
    $offset_seconds = $dtz->getOffset($dt);
1232
  }
1233

    
1234
  switch ($db_type) {
1235
    case 'mysql':
1236
      switch ($field_type) {
1237
        case 'int':
1238
          $field = "DATE_ADD('19700101', INTERVAL $field SECOND)";
1239
          break;
1240
        case 'datetime':
1241
          break;
1242
      }
1243
      if (!empty($offset)) {
1244
        $field = "($field + INTERVAL $offset_seconds SECOND)";
1245
      }
1246
      return $field;
1247
    case 'pgsql':
1248
      switch ($field_type) {
1249
        case 'int':
1250
          $field = "TO_TIMESTAMP($field)";
1251
          break;
1252
        case 'datetime':
1253
          break;
1254
      }
1255
      if (!empty($offset)) {
1256
        $field = "($field + INTERVAL '$offset_seconds SECONDS')";
1257
      }
1258
      return $field;
1259
    case 'sqlite':
1260
      if (!empty($offset)) {
1261
        $field = "($field + '$offset_seconds')";
1262
      }
1263
      return $field;
1264
  }
1265
}
1266

    
1267
/**
1268
 * Helper function to create cross-database SQL date formatting.
1269
 *
1270
 * @param $format
1271
 *   A format string for the result, like 'Y-m-d H:i:s'.
1272
 * @param $field
1273
 *   The real table and field name, like 'tablename.fieldname'.
1274
 * @param $field_type
1275
 *   The type of date field, 'int' or 'datetime'.
1276
 * @param $set_offset
1277
 *   The name of a field that holds the timezone offset or a fixed timezone
1278
 *   offset value. If not provided, the normal Drupal timezone handling
1279
 *   will be used, i.e. $set_offset = 0 will make no timezone adjustment.
1280
 * @return
1281
 *   An appropriate SQL string for the db type and field type.
1282
 */
1283
function views_date_sql_format($format, $field, $field_type = 'int', $set_offset = NULL) {
1284
  $db_type = Database::getConnection()->databaseType();
1285
  $field = views_date_sql_field($field, $field_type, $set_offset);
1286
  switch ($db_type) {
1287
    case 'mysql':
1288
      $replace = array(
1289
        'Y' => '%Y',
1290
        'y' => '%y',
1291
        'M' => '%b',
1292
        'm' => '%m',
1293
        'n' => '%c',
1294
        'F' => '%M',
1295
        'D' => '%a',
1296
        'd' => '%d',
1297
        'l' => '%W',
1298
        'j' => '%e',
1299
        'W' => '%v',
1300
        'H' => '%H',
1301
        'h' => '%h',
1302
        'i' => '%i',
1303
        's' => '%s',
1304
        'A' => '%p',
1305
        );
1306
      $format = strtr($format, $replace);
1307
      return "DATE_FORMAT($field, '$format')";
1308
    case 'pgsql':
1309
      $replace = array(
1310
        'Y' => 'YYYY',
1311
        'y' => 'YY',
1312
        'M' => 'Mon',
1313
        'm' => 'MM',
1314
        'n' => 'MM', // no format for Numeric representation of a month, without leading zeros
1315
        'F' => 'Month',
1316
        'D' => 'Dy',
1317
        'd' => 'DD',
1318
        'l' => 'Day',
1319
        'j' => 'DD', // no format for Day of the month without leading zeros
1320
        'W' => 'WW',
1321
        'H' => 'HH24',
1322
        'h' => 'HH12',
1323
        'i' => 'MI',
1324
        's' => 'SS',
1325
        'A' => 'AM',
1326
        );
1327
      $format = strtr($format, $replace);
1328
      return "TO_CHAR($field, '$format')";
1329
    case 'sqlite':
1330
      $replace = array(
1331
        'Y' => '%Y', // 4 digit year number
1332
        'y' => '%Y', // no format for 2 digit year number
1333
        'M' => '%m', // no format for 3 letter month name
1334
        'm' => '%m', // month number with leading zeros
1335
        'n' => '%m', // no format for month number without leading zeros
1336
        'F' => '%m', // no format for full month name
1337
        'D' => '%d', // no format for 3 letter day name
1338
        'd' => '%d', // day of month number with leading zeros
1339
        'l' => '%d', // no format for full day name
1340
        'j' => '%d', // no format for day of month number without leading zeros
1341
        'W' => '%W', // ISO week number
1342
        'H' => '%H', // 24 hour hour with leading zeros
1343
        'h' => '%H', // no format for 12 hour hour with leading zeros
1344
        'i' => '%M', // minutes with leading zeros
1345
        's' => '%S', // seconds with leading zeros
1346
        'A' => '', // no format for  AM/PM
1347
      );
1348
      $format = strtr($format, $replace);
1349
      return "strftime('$format', $field, 'unixepoch')";
1350
  }
1351
}
1352

    
1353
/**
1354
 * Helper function to create cross-database SQL date extraction.
1355
 *
1356
 * @param $extract_type
1357
 *   The type of value to extract from the date, like 'MONTH'.
1358
 * @param $field
1359
 *   The real table and field name, like 'tablename.fieldname'.
1360
 * @param $field_type
1361
 *   The type of date field, 'int' or 'datetime'.
1362
 * @param $set_offset
1363
 *   The name of a field that holds the timezone offset or a fixed timezone
1364
 *   offset value. If not provided, the normal Drupal timezone handling
1365
 *   will be used, i.e. $set_offset = 0 will make no timezone adjustment.
1366
 * @return
1367
 *   An appropriate SQL string for the db type and field type.
1368
 */
1369
function views_date_sql_extract($extract_type, $field, $field_type = 'int', $set_offset = NULL) {
1370
  $db_type = Database::getConnection()->databaseType();
1371
  $field = views_date_sql_field($field, $field_type, $set_offset);
1372

    
1373
  // Note there is no space after FROM to avoid db_rewrite problems
1374
  // see http://drupal.org/node/79904.
1375
  switch ($extract_type) {
1376
  case('DATE'):
1377
    return $field;
1378
  case('YEAR'):
1379
    return "EXTRACT(YEAR FROM($field))";
1380
  case('MONTH'):
1381
    return "EXTRACT(MONTH FROM($field))";
1382
  case('DAY'):
1383
    return "EXTRACT(DAY FROM($field))";
1384
  case('HOUR'):
1385
    return "EXTRACT(HOUR FROM($field))";
1386
  case('MINUTE'):
1387
    return "EXTRACT(MINUTE FROM($field))";
1388
  case('SECOND'):
1389
    return "EXTRACT(SECOND FROM($field))";
1390
  case('WEEK'):  // ISO week number for date
1391
    switch ($db_type) {
1392
      case('mysql'):
1393
        // WEEK using arg 3 in mysql should return the same value as postgres EXTRACT
1394
        return "WEEK($field, 3)";
1395
      case('pgsql'):
1396
        return "EXTRACT(WEEK FROM($field))";
1397
    }
1398
  case('DOW'):
1399
    switch ($db_type) {
1400
      case('mysql'):
1401
        // mysql returns 1 for Sunday through 7 for Saturday
1402
        // php date functions and postgres use 0 for Sunday and 6 for Saturday
1403
        return "INTEGER(DAYOFWEEK($field) - 1)";
1404
      case('pgsql'):
1405
        return "EXTRACT(DOW FROM($field))";
1406
    }
1407
  case('DOY'):
1408
    switch ($db_type) {
1409
      case('mysql'):
1410
        return "DAYOFYEAR($field)";
1411
      case('pgsql'):
1412
        return "EXTRACT(DOY FROM($field))";
1413
    }
1414
  }
1415
}
1416

    
1417
/**
1418
 * @}
1419
 */
1420

    
1421
/**
1422
 * @defgroup views_join_handlers Views join handlers
1423
 * @{
1424
 * Handlers to tell Views how to join tables together.
1425
 *
1426
 * Here is how you do complex joins:
1427
 *
1428
 * @code
1429
 * class views_join_complex extends views_join {
1430
 *   // PHP 4 doesn't call constructors of the base class automatically from a
1431
 *   // constructor of a derived class. It is your responsibility to propagate
1432
 *   // the call to constructors upstream where appropriate.
1433
 *   function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') {
1434
 *     parent::construct($table, $left_table, $left_field, $field, $extra, $type);
1435
 *   }
1436
 *
1437
 *   function build_join($select_query, $table, $view_query) {
1438
 *     $this->extra = 'foo.bar = baz.boing';
1439
 *     parent::build_join($select_query, $table, $view_query);
1440
 *   }
1441
 * }
1442
 * @endcode
1443
 */
1444

    
1445
/**
1446
 * A function class to represent a join and create the SQL necessary
1447
 * to implement the join.
1448
 *
1449
 * This is the Delegation pattern. If we had PHP5 exclusively, we would
1450
 * declare this an interface.
1451
 *
1452
 * Extensions of this class can be used to create more interesting joins.
1453
 *
1454
 * join definition
1455
 *   - table: table to join (right table)
1456
 *   - field: field to join on (right field)
1457
 *   - left_table: The table we join to
1458
 *   - left_field: The field we join to
1459
 *   - type: either LEFT (default) or INNER
1460
 *   - extra: An array of extra conditions on the join. Each condition is
1461
 *     either a string that's directly added, or an array of items:
1462
 *   - - table: If not set, current table; if NULL, no table. If you specify a
1463
 *       table in cached definition, Views will try to load from an existing
1464
 *       alias. If you use realtime joins, it works better.
1465
 *   - - field: Field or formula
1466
 *       in formulas we can reference the right table by using %alias
1467
 *       @see SelectQueryInterface::addJoin()
1468
 *   - - operator: defaults to =
1469
 *   - - value: Must be set. If an array, operator will be defaulted to IN.
1470
 *   - - numeric: If true, the value will not be surrounded in quotes.
1471
 *   - - extra type: How all the extras will be combined. Either AND or OR. Defaults to AND.
1472
 */
1473
class views_join {
1474
  var $table = NULL;
1475
  var $left_table = NULL;
1476
  var $left_field = NULL;
1477
  var $field = NULL;
1478
  var $extra = NULL;
1479
  var $type = NULL;
1480
  var $definition = array();
1481

    
1482
  /**
1483
   * Construct the views_join object.
1484
   */
1485
  function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') {
1486
    $this->extra_type = 'AND';
1487
    if (!empty($table)) {
1488
      $this->table = $table;
1489
      $this->left_table = $left_table;
1490
      $this->left_field = $left_field;
1491
      $this->field = $field;
1492
      $this->extra = $extra;
1493
      $this->type = strtoupper($type);
1494
    }
1495
    elseif (!empty($this->definition)) {
1496
      // if no arguments, construct from definition.
1497
      // These four must exist or it will throw notices.
1498
      $this->table = $this->definition['table'];
1499
      $this->left_table = $this->definition['left_table'];
1500
      $this->left_field = $this->definition['left_field'];
1501
      $this->field = $this->definition['field'];
1502
      if (!empty($this->definition['extra'])) {
1503
        $this->extra = $this->definition['extra'];
1504
      }
1505
      if (!empty($this->definition['extra type'])) {
1506
        $this->extra_type = strtoupper($this->definition['extra type']);
1507
      }
1508

    
1509
      $this->type = !empty($this->definition['type']) ? strtoupper($this->definition['type']) : 'LEFT';
1510
    }
1511
  }
1512

    
1513
  /**
1514
   * Build the SQL for the join this object represents.
1515
   *
1516
   * When possible, try to use table alias instead of table names.
1517
   *
1518
   * @param $select_query
1519
   *   An implementation of SelectQueryInterface.
1520
   * @param $table
1521
   *   The base table to join.
1522
   * @param $view_query
1523
   *   The source query, implementation of views_plugin_query.
1524
   */
1525
  function build_join($select_query, $table, $view_query) {
1526
    if (empty($this->definition['table formula'])) {
1527
      $right_table = $this->table;
1528
    }
1529
    else {
1530
      $right_table = $this->definition['table formula'];
1531
    }
1532

    
1533
    if ($this->left_table) {
1534
      $left = $view_query->get_table_info($this->left_table);
1535
      $left_field = "$left[alias].$this->left_field";
1536
    }
1537
    else {
1538
      // This can be used if left_field is a formula or something. It should be used only *very* rarely.
1539
      $left_field = $this->left_field;
1540
    }
1541

    
1542
    $condition = "$left_field = $table[alias].$this->field";
1543
    $arguments = array();
1544

    
1545
    // Tack on the extra.
1546
    if (isset($this->extra)) {
1547
      if (is_array($this->extra)) {
1548
        $extras = array();
1549
        foreach ($this->extra as $info) {
1550
          $extra = '';
1551
          // Figure out the table name. Remember, only use aliases provided
1552
          // if at all possible.
1553
          $join_table = '';
1554
          if (!array_key_exists('table', $info)) {
1555
            $join_table = $table['alias'] . '.';
1556
          }
1557
          elseif (isset($info['table'])) {
1558
            // If we're aware of a table alias for this table, use the table
1559
            // alias instead of the table name.
1560
            if (isset($left) && $left['table'] == $info['table']) {
1561
              $join_table = $left['alias'] . '.';
1562
            }
1563
            else {
1564
              $join_table = $info['table'] . '.';
1565
            }
1566
          }
1567

    
1568
          // Convert a single-valued array of values to the single-value case,
1569
          // and transform from IN() notation to = notation
1570
          if (is_array($info['value']) && count($info['value']) == 1) {
1571
            if (empty($info['operator'])) {
1572
              $operator = '=';
1573
            }
1574
            else {
1575
              $operator = $info['operator'] == 'NOT IN' ? '!=' : '=';
1576
            }
1577
            $info['value'] = array_shift($info['value']);
1578
          }
1579

    
1580
          if (is_array($info['value'])) {
1581
            // With an array of values, we need multiple placeholders and the
1582
            // 'IN' operator is implicit.
1583
            foreach ($info['value'] as $value) {
1584
              $placeholder_i = $view_query->placeholder('views_join_condition_');
1585
              $arguments[$placeholder_i] = $value;
1586
            }
1587

    
1588
            $operator = !empty($info['operator']) ? $info['operator'] : 'IN';
1589
            $placeholder = '( ' . implode(', ', array_keys($arguments)) . ' )';
1590
          }
1591
          else {
1592
            // With a single value, the '=' operator is implicit.
1593
            $operator = !empty($info['operator']) ? $info['operator'] : '=';
1594
            $placeholder = $view_query->placeholder('views_join_condition_');
1595
            $arguments[$placeholder] = $info['value'];
1596
          }
1597
          $extras[] = "$join_table$info[field] $operator $placeholder";
1598
        }
1599

    
1600
        if ($extras) {
1601
          if (count($extras) == 1) {
1602
            $condition .= ' AND ' . array_shift($extras);
1603
          }
1604
          else {
1605
            $condition .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')';
1606
          }
1607
        }
1608
      }
1609
      elseif ($this->extra && is_string($this->extra)) {
1610
        $condition .= " AND ($this->extra)";
1611
      }
1612
    }
1613

    
1614
    $select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments);
1615
  }
1616
}
1617

    
1618
/**
1619
 * Join handler for relationships that join with a subquery as the left field.
1620
 * eg:
1621
 *  LEFT JOIN node node_term_data ON ([YOUR SUBQUERY HERE]) = node_term_data.nid
1622
 *
1623
 * join definition
1624
 *   same as views_join class above, except:
1625
 *   - left_query: The subquery to use in the left side of the join clause.
1626
 */
1627
class views_join_subquery extends views_join {
1628
  function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') {
1629
    parent::construct($table, $left_table, $left_field, $field, $extra, $type);
1630
    $this->left_query = $this->definition['left_query'];
1631
  }
1632

    
1633
  /**
1634
   * Build the SQL for the join this object represents.
1635
   *
1636
   * @param $select_query
1637
   *   An implementation of SelectQueryInterface.
1638
   * @param $table
1639
   *   The base table to join.
1640
   * @param $view_query
1641
   *   The source query, implementation of views_plugin_query.
1642
   * @return
1643
   *
1644
   */
1645
  function build_join($select_query, $table, $view_query) {
1646
    if (empty($this->definition['table formula'])) {
1647
      $right_table = "{" . $this->table . "}";
1648
    }
1649
    else {
1650
      $right_table = $this->definition['table formula'];
1651
    }
1652

    
1653
    // Add our join condition, using a subquery on the left instead of a field.
1654
    $condition = "($this->left_query) = $table[alias].$this->field";
1655
    $arguments = array();
1656

    
1657
    // Tack on the extra.
1658
    // This is just copied verbatim from the parent class, which itself has a bug: http://drupal.org/node/1118100
1659
    if (isset($this->extra)) {
1660
      if (is_array($this->extra)) {
1661
        $extras = array();
1662
        foreach ($this->extra as $info) {
1663
          $extra = '';
1664
          // Figure out the table name. Remember, only use aliases provided
1665
          // if at all possible.
1666
          $join_table = '';
1667
          if (!array_key_exists('table', $info)) {
1668
            $join_table = $table['alias'] . '.';
1669
          }
1670
          elseif (isset($info['table'])) {
1671
            $join_table = $info['table'] . '.';
1672
          }
1673

    
1674
          $placeholder = ':views_join_condition_' . $select_query->nextPlaceholder();
1675

    
1676
          if (is_array($info['value'])) {
1677
            $operator = !empty($info['operator']) ? $info['operator'] : 'IN';
1678
            // Transform from IN() notation to = notation if just one value.
1679
            if (count($info['value']) == 1) {
1680
              $info['value'] = array_shift($info['value']);
1681
              $operator = $operator == 'NOT IN' ? '!=' : '=';
1682
            }
1683
          }
1684
          else {
1685
            $operator = !empty($info['operator']) ? $info['operator'] : '=';
1686
          }
1687

    
1688
          $extras[] = "$join_table$info[field] $operator $placeholder";
1689
          $arguments[$placeholder] = $info['value'];
1690
        }
1691

    
1692
        if ($extras) {
1693
          if (count($extras) == 1) {
1694
            $condition .= ' AND ' . array_shift($extras);
1695
          }
1696
          else {
1697
            $condition .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')';
1698
          }
1699
        }
1700
      }
1701
      elseif ($this->extra && is_string($this->extra)) {
1702
        $condition .= " AND ($this->extra)";
1703
      }
1704
    }
1705

    
1706
    $select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments);
1707
  }
1708
}
1709

    
1710
/**
1711
 * @}
1712
 */