Projet

Général

Profil

Paste
Télécharger (55,4 ko) Statistiques
| Branche: | Révision:

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

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
 *                    public 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
  /**
157
   * The top object of a view.
158
   *
159
   * @var view
160
   */
161
  public $view = NULL;
162

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

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

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

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

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

    
191
  /**
192
   * Init the handler with necessary data.
193
   *
194
   * @param view $view
195
   *   The $view object this handler is attached to.
196
   * @param array $options
197
   *   The item from the database; the actual contents of this will vary
198
   *   based upon the type of handler.
199
   */
200
  public function init(&$view, &$options) {
201
    $this->view = &$view;
202
    $display_id = $this->view->current_display;
203
    // Check to see if this handler type is defaulted. Note that
204
    // we have to do a lookup because the type is singular but the
205
    // option is stored as the plural.
206
    // If the 'moved to' keyword moved our handler, let's fix that now.
207
    if (isset($this->actual_table)) {
208
      $options['table'] = $this->actual_table;
209
    }
210

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

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

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

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

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

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

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

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

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

    
256
  /**
257
   * {@inheritdoc}
258
   */
259
  public function option_definition() {
260
    $options = parent::option_definition();
261

    
262
    $options['id'] = array('default' => '');
263
    $options['table'] = array('default' => '');
264
    $options['field'] = array('default' => '');
265
    $options['relationship'] = array('default' => 'none');
266
    $options['group_type'] = array('default' => 'group');
267
    $options['ui_name'] = array('default' => '');
268

    
269
    return $options;
270
  }
271

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

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

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

    
313
    return $field;
314
  }
315

    
316
  /**
317
   * Sanitize the value for output.
318
   *
319
   * @param string $value
320
   *   The value being rendered.
321
   * @param string $type
322
   *   The type of sanitization needed. If not provided, check_plain() is used.
323
   *
324
   * @return string
325
   *   Returns the safe value.
326
   */
327
  public function sanitize_value($value, $type = NULL) {
328
    switch ($type) {
329
      case 'xss':
330
        $value = filter_xss($value);
331
        break;
332

    
333
      case 'xss_admin':
334
        $value = filter_xss_admin($value);
335
        break;
336

    
337
      case 'url':
338
        $value = check_url($value);
339
        break;
340

    
341
      default:
342
        $value = check_plain($value);
343
        break;
344
    }
345
    return $value;
346
  }
347

    
348
  /**
349
   * Transform a string by a certain method.
350
   *
351
   * @param string $string
352
   *    The input you want to transform.
353
   * @param array $option
354
   *    How do you want to transform it, possible values:
355
   *      - upper: Uppercase the string.
356
   *      - lower: lowercase the string.
357
   *      - ucfirst: Make the first char uppercase.
358
   *      - ucwords: Make each word in the string uppercase.
359
   *
360
   * @return string
361
   *    The transformed string.
362
   */
363
  public function case_transform($string, $option) {
364
    global $multibyte;
365

    
366
    switch ($option) {
367
      default:
368
        return $string;
369
      case 'upper':
370
        return drupal_strtoupper($string);
371

    
372
      case 'lower':
373
        return drupal_strtolower($string);
374

    
375
      case 'ucfirst':
376
        return drupal_strtoupper(drupal_substr($string, 0, 1)) . drupal_substr($string, 1);
377

    
378
      case 'ucwords':
379
        if ($multibyte == UNICODE_MULTIBYTE) {
380
          return mb_convert_case($string, MB_CASE_TITLE);
381
        }
382
        else {
383
          return ucwords($string);
384
        }
385
    }
386
  }
387

    
388
  /**
389
   * Validate the options form.
390
   */
391
  public function options_validate(&$form, &$form_state) {
392
  }
393

    
394
  /**
395
   * Build the options form.
396
   */
397
  public function options_form(&$form, &$form_state) {
398
    // Some form elements belong in a fieldset for presentation, but can't
399
    // be moved into one because of the form_state['values'] hierarchy. Those
400
    // elements can add a #fieldset => 'fieldset_name' property, and they'll
401
    // be moved to their fieldset during pre_render.
402
    $form['#pre_render'][] = 'views_ui_pre_render_add_fieldset_markup';
403

    
404
    $form['ui_name'] = array(
405
      '#type' => 'textfield',
406
      '#title' => t('Administrative title'),
407
      '#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.'),
408
      '#default_value' => $this->options['ui_name'],
409
      '#fieldset' => 'more',
410
    );
411

    
412
    // This form is long and messy enough that the "Administrative title" option
413
    // belongs in a "more options" fieldset at the bottom of the form.
414
    $form['more'] = array(
415
      '#type' => 'fieldset',
416
      '#title' => t('More'),
417
      '#collapsible' => TRUE,
418
      '#collapsed' => TRUE,
419
      '#weight' => 150,
420
    );
421
    // Allow to alter the default values brought into the form.
422
    drupal_alter('views_handler_options', $this->options, $view);
423
  }
424

    
425
  /**
426
   * Perform any necessary changes to the form values prior to storage.
427
   * There is no need for this function to actually store the data.
428
   */
429
  public function options_submit(&$form, &$form_state) {
430
  }
431

    
432
  /**
433
   * Provides the handler some groupby.
434
   */
435
  public function use_group_by() {
436
    return TRUE;
437
  }
438

    
439
  /**
440
   * Provide a form for aggregation settings.
441
   */
442
  public function groupby_form(&$form, &$form_state) {
443
    $view = &$form_state['view'];
444
    $display_id = $form_state['display_id'];
445
    $types = views_object_types();
446
    $type = $form_state['type'];
447
    $id = $form_state['id'];
448

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

    
452
    $form['#section'] = $display_id . '-' . $type . '-' . $id;
453

    
454
    $view->init_query();
455
    $info = $view->query->get_aggregation_info();
456
    foreach ($info as $id => $aggregate) {
457
      $group_types[$id] = $aggregate['title'];
458
    }
459

    
460
    $form['group_type'] = array(
461
      '#type' => 'select',
462
      '#title' => t('Aggregation type'),
463
      '#default_value' => $this->options['group_type'],
464
      '#description' => t('Select the aggregation function to use on this field.'),
465
      '#options' => $group_types,
466
    );
467
  }
468

    
469
  /**
470
   * Perform any necessary changes to the form values prior to storage.
471
   * There is no need for this function to actually store the data.
472
   */
473
  public function groupby_form_submit(&$form, &$form_state) {
474
    $item =& $form_state['handler']->options;
475

    
476
    $item['group_type'] = $form_state['values']['options']['group_type'];
477
  }
478

    
479
  /**
480
   * If a handler has 'extra options' it will get a little settings widget and
481
   * another form called extra_options.
482
   */
483
  public function has_extra_options() {
484
    return FALSE;
485
  }
486

    
487
  /**
488
   * Provide defaults for the handler.
489
   */
490
  public function extra_options(&$option) {
491
  }
492

    
493
  /**
494
   * Provide a form for setting options.
495
   */
496
  public function extra_options_form(&$form, &$form_state) {
497
  }
498

    
499
  /**
500
   * Validate the options form.
501
   */
502
  public function extra_options_validate($form, &$form_state) {
503
  }
504

    
505
  /**
506
   * Perform any necessary changes to the form values prior to storage.
507
   * There is no need for this function to actually store the data.
508
   */
509
  public function extra_options_submit($form, &$form_state) {
510
  }
511

    
512
  /**
513
   * Determine if a handler can be exposed.
514
   */
515
  public function can_expose() {
516
    return FALSE;
517
  }
518

    
519
  /**
520
   * Set new exposed option defaults when exposed setting is flipped
521
   * on.
522
   */
523
  public function expose_options() {
524
  }
525

    
526
  /**
527
   * Get information about the exposed form for the form renderer.
528
   */
529
  public function exposed_info() {
530
  }
531

    
532
  /**
533
   * Render our chunk of the exposed handler form when selecting.
534
   */
535
  public function exposed_form(&$form, &$form_state) {
536
  }
537

    
538
  /**
539
   * Validate the exposed handler form.
540
   */
541
  public function exposed_validate(&$form, &$form_state) {
542
  }
543

    
544
  /**
545
   * Submit the exposed handler form.
546
   */
547
  public function exposed_submit(&$form, &$form_state) {
548
  }
549

    
550
  /**
551
   * Form for exposed handler options.
552
   */
553
  public function expose_form(&$form, &$form_state) {
554
  }
555

    
556
  /**
557
   * Validate the options form.
558
   */
559
  public function expose_validate($form, &$form_state) {
560
  }
561

    
562
  /**
563
   * Perform any necessary changes to the form exposes prior to storage.
564
   * There is no need for this function to actually store the data.
565
   */
566
  public function expose_submit($form, &$form_state) {
567
  }
568

    
569
  /**
570
   * Shortcut to display the expose/hide button.
571
   */
572
  public function show_expose_button(&$form, &$form_state) {
573
  }
574

    
575
  /**
576
   * Shortcut to display the exposed options form.
577
   */
578
  public function show_expose_form(&$form, &$form_state) {
579
    if (empty($this->options['exposed'])) {
580
      return;
581
    }
582

    
583
    $this->expose_form($form, $form_state);
584

    
585
    // When we click the expose button, we add new gadgets to the form but they
586
    // have no data in $_POST so their defaults get wiped out. This prevents
587
    // these defaults from getting wiped out. This setting will only be TRUE
588
    // during a 2nd pass rerender.
589
    if (!empty($form_state['force_expose_options'])) {
590
      foreach (element_children($form['expose']) as $id) {
591
        if (isset($form['expose'][$id]['#default_value']) && !isset($form['expose'][$id]['#value'])) {
592
          $form['expose'][$id]['#value'] = $form['expose'][$id]['#default_value'];
593
        }
594
      }
595
    }
596
  }
597

    
598
  /**
599
   * Check whether current user has access to this handler.
600
   *
601
   * @return bool
602
   */
603
  public function access() {
604
    if (isset($this->definition['access callback']) && function_exists($this->definition['access callback'])) {
605
      if (isset($this->definition['access arguments']) && is_array($this->definition['access arguments'])) {
606
        return call_user_func_array($this->definition['access callback'], $this->definition['access arguments']);
607
      }
608
      return $this->definition['access callback']();
609
    }
610

    
611
    return TRUE;
612
  }
613

    
614
  /**
615
   * Run before the view is built.
616
   *
617
   * This gives all the handlers some time to set up before any handler has
618
   * been fully run.
619
   */
620
  public function pre_query() {
621
  }
622

    
623
  /**
624
   * Run after the view is executed, before the result is cached.
625
   *
626
   * This gives all the handlers some time to modify values. This is primarily
627
   * used so that handlers that pull up secondary data can put it in the
628
   * $values so that the raw data can be utilized externally.
629
   */
630
  public function post_execute(&$values) {
631
  }
632

    
633
  /**
634
   * Provides a unique placeholders for handlers.
635
   */
636
  public function placeholder() {
637
    return $this->query->placeholder($this->options['table'] . '_' . $this->options['field']);
638
  }
639

    
640
  /**
641
   * Called just prior to query(), this lets a handler set up any relationship
642
   * it needs.
643
   */
644
  public function set_relationship() {
645
    // Ensure this gets set to something.
646
    $this->relationship = NULL;
647

    
648
    // Don't process non-existant relationships.
649
    if (empty($this->options['relationship']) || $this->options['relationship'] == 'none') {
650
      return;
651
    }
652

    
653
    $relationship = $this->options['relationship'];
654

    
655
    // Ignore missing/broken relationships.
656
    if (empty($this->view->relationship[$relationship])) {
657
      return;
658
    }
659

    
660
    // Check to see if the relationship has already processed. If not, then we
661
    // cannot process it.
662
    if (empty($this->view->relationship[$relationship]->alias)) {
663
      return;
664
    }
665

    
666
    // Finally!
667
    $this->relationship = $this->view->relationship[$relationship]->alias;
668
  }
669

    
670
  /**
671
   * Ensure the main table for this handler is in the query. This is used
672
   * a lot.
673
   */
674
  public function ensure_my_table() {
675
    if (!isset($this->table_alias)) {
676
      if (!method_exists($this->query, 'ensure_table')) {
677
        vpr(t('Ensure my table called but query has no ensure_table method.'));
678
        return;
679
      }
680
      $this->table_alias = $this->query->ensure_table($this->table, $this->relationship);
681
    }
682
    return $this->table_alias;
683
  }
684

    
685
  /**
686
   * Provide text for the administrative summary.
687
   */
688
  public function admin_summary() {
689
  }
690

    
691
  /**
692
   * Determine if the argument needs a style plugin.
693
   *
694
   * @return bool
695
   */
696
  public function needs_style_plugin() {
697
    return FALSE;
698
  }
699

    
700
  /**
701
   * Determine if this item is 'exposed', meaning it provides form elements
702
   * to let users modify the view.
703
   *
704
   * @return bool
705
   */
706
  public function is_exposed() {
707
    return !empty($this->options['exposed']);
708
  }
709

    
710
  /**
711
   * Returns TRUE if the exposed filter works like a grouped filter.
712
   */
713
  public function is_a_group() {
714
    return FALSE;
715
  }
716

    
717
  /**
718
   * Define if the exposed input has to be submitted multiple times.
719
   * This is TRUE when exposed filters grouped are using checkboxes as
720
   * widgets.
721
   */
722
  public function multiple_exposed_input() {
723
    return FALSE;
724
  }
725

    
726
  /**
727
   * Take input from exposed handlers and assign to this handler, if necessary.
728
   */
729
  public function accept_exposed_input($input) {
730
    return TRUE;
731
  }
732

    
733
  /**
734
   * If set to remember exposed input in the session, store it there.
735
   */
736
  public function store_exposed_input($input, $status) {
737
    return TRUE;
738
  }
739

    
740
  /**
741
   * Get the join object that should be used for this handler.
742
   *
743
   * This method isn't used a great deal, but it's very handy for easily
744
   * getting the join if it is necessary to make some changes to it, such
745
   * as adding an 'extra'.
746
   */
747
  public function get_join() {
748
    // get the join from this table that links back to the base table.
749
    // Determine the primary table to seek.
750
    if (empty($this->query->relationships[$this->relationship])) {
751
      $base_table = $this->query->base_table;
752
    }
753
    else {
754
      $base_table = $this->query->relationships[$this->relationship]['base'];
755
    }
756

    
757
    $join = views_get_table_join($this->table, $base_table);
758
    if ($join) {
759
      return clone $join;
760
    }
761
  }
762

    
763
  /**
764
   * Validates the handler against the complete View.
765
   *
766
   * This is called when the complete View is being validated. For validating
767
   * the handler options form use options_validate().
768
   *
769
   * @see views_handler::options_validate()
770
   *
771
   * @return array
772
   *   Empty array if the handler is valid; an array of error strings if it is
773
   *   not.
774
   */
775
  public function validate() {
776
    return array();
777
  }
778

    
779
  /**
780
   * Determine if the handler is considered 'broken'.
781
   *
782
   * Generally only returns TRUE if the handler can't be found.
783
   *
784
   * @return bool
785
   *   The handler could not be loaded.
786
   */
787
  public function broken() {
788
  }
789

    
790
}
791

    
792
/**
793
 * This many to one helper object is used on both arguments and filters.
794
 *
795
 * @todo This requires extensive documentation on how this class is to
796
 * be used. For now, look at the arguments and filters that use it. Lots
797
 * of stuff is just pass-through but there are definitely some interesting
798
 * areas where they interact.
799
 *
800
 * Any handler that uses this can have the following possibly additional
801
 * definition terms:
802
 * - numeric: If true, treat this field as numeric, using %d instead of %s in
803
 *            queries.
804
 */
805
class views_many_to_one_helper {
806

    
807
  /**
808
   * Contains possible existing placeholders used by the query.
809
   *
810
   * @var array
811
   */
812
  public $placeholders = array();
813

    
814
  /**
815
   * {@inheritdoc}
816
   */
817
  public function __construct(&$handler) {
818
    $this->handler = &$handler;
819
  }
820

    
821
  /**
822
   * {@inheritdoc}
823
   */
824
  static function option_definition(&$options) {
825
    $options['reduce_duplicates'] = array('default' => FALSE, 'bool' => TRUE);
826
  }
827

    
828
  /**
829
   * {@inheritdoc}
830
   */
831
  public function options_form(&$form, &$form_state) {
832
    $form['reduce_duplicates'] = array(
833
      '#type' => 'checkbox',
834
      '#title' => t('Reduce duplicates'),
835
      '#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.'),
836
      '#default_value' => !empty($this->handler->options['reduce_duplicates']),
837
      '#weight' => 4,
838
    );
839
  }
840

    
841
  /**
842
   * Provide an option to use a formula.
843
   *
844
   * If it wants us to do this, it must set $helper->formula = TRUE and
845
   * implement handler->get_formula();.
846
   */
847
  public function get_field() {
848
    if (!empty($this->formula)) {
849
      return $this->handler->get_formula();
850
    }
851
    else {
852
      return $this->handler->table_alias . '.' . $this->handler->real_field;
853
    }
854
  }
855

    
856
  /**
857
   * Add a table to the query.
858
   *
859
   * This is an advanced concept; not only does it add a new instance of the
860
   * table, but it follows the relationship path all the way down to the
861
   * relationship link point and adds *that* as a new relationship and then adds
862
   * the table to the relationship, if necessary.
863
   */
864
  public function add_table($join = NULL, $alias = NULL) {
865
    // This is used for lookups in the many_to_one table.
866
    $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
867

    
868
    if (empty($join)) {
869
      $join = $this->get_join();
870
    }
871

    
872
    // See if there's a chain between us and the base relationship. If so, we
873
    // need to create a new relationship to use.
874
    $relationship = $this->handler->relationship;
875

    
876
    // Determine the primary table to seek.
877
    if (empty($this->handler->query->relationships[$relationship])) {
878
      $base_table = $this->handler->query->base_table;
879
    }
880
    else {
881
      $base_table = $this->handler->query->relationships[$relationship]['base'];
882
    }
883

    
884
    // Cycle through the joins. This isn't as error-safe as the normal
885
    // ensure_path logic. Perhaps it should be.
886
    $r_join = clone $join;
887
    while (!empty($r_join) && $r_join->left_table != $base_table) {
888
      $r_join = views_get_table_join($r_join->left_table, $base_table);
889
    }
890
    // If we found that there are tables in between, add the relationship.
891
    if ($r_join->table != $join->table) {
892
      $relationship = $this->handler->query->add_relationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship);
893
    }
894

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

    
898
    // Store what values are used by this table chain so that other chains can
899
    // automatically discard those values.
900
    if (empty($this->handler->view->many_to_one_tables[$field])) {
901
      $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
902
    }
903
    else {
904
      $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value);
905
    }
906

    
907
    return $alias;
908
  }
909

    
910
  /**
911
   * {@inheritdoc}
912
   */
913
  public function get_join() {
914
    return $this->handler->get_join();
915
  }
916

    
917
  /**
918
   * Provide the proper join for summary queries.
919
   *
920
   * This is important in part because it will cooperate with other arguments if
921
   * possible.
922
   */
923
  public function summary_join() {
924
    $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
925
    $join = $this->get_join();
926

    
927
    // Shortcuts.
928
    $options = $this->handler->options;
929
    $view = &$this->handler->view;
930
    $query = &$this->handler->query;
931

    
932
    if (!empty($options['require_value'])) {
933
      $join->type = 'INNER';
934
    }
935

    
936
    if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) {
937
      return $query->ensure_table($this->handler->table, $this->handler->relationship, $join);
938
    }
939
    else {
940
      if (!empty($view->many_to_one_tables[$field])) {
941
        foreach ($view->many_to_one_tables[$field] as $value) {
942
          $join->extra = array(
943
            array(
944
              'field' => $this->handler->real_field,
945
              'operator' => '!=',
946
              'value' => $value,
947
              'numeric' => !empty($this->definition['numeric']),
948
            ),
949
          );
950
        }
951
      }
952
      return $this->add_table($join);
953
    }
954
  }
955

    
956
  /**
957
   * Override ensure_my_table so we can control how this joins in.
958
   *
959
   * The operator actually has influence over joining.
960
   */
961
  public function ensure_my_table() {
962
    if (!isset($this->handler->table_alias)) {
963
      // Case 1: Operator is an 'or' and we're not reducing duplicates.
964
      // We hence get the absolute simplest.
965
      $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
966
      if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) {
967
        if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) {
968
          // Query optimization, INNER joins are slightly faster, so use them
969
          // when we know we can.
970
          $join = $this->get_join();
971
          if (isset($join)) {
972
            $join->type = 'INNER';
973
          }
974
          $this->handler->table_alias = $this->handler->query->ensure_table($this->handler->table, $this->handler->relationship, $join);
975
          $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
976
        }
977
        else {
978
          $join = $this->get_join();
979
          $join->type = 'LEFT';
980
          if (!empty($this->handler->view->many_to_one_tables[$field])) {
981
            foreach ($this->handler->view->many_to_one_tables[$field] as $value) {
982
              $join->extra = array(
983
                array(
984
                  'field' => $this->handler->real_field,
985
                  'operator' => '!=',
986
                  'value' => $value,
987
                  'numeric' => !empty($this->handler->definition['numeric']),
988
                ),
989
              );
990
            }
991
          }
992

    
993
          $this->handler->table_alias = $this->add_table($join);
994
        }
995

    
996
        return $this->handler->table_alias;
997
      }
998

    
999
      // Case 2: it's an 'and' or an 'or'.
1000
      // We do one join per selected value.
1001
      if ($this->handler->operator != 'not') {
1002
        // Clone the join for each table.
1003
        $this->handler->table_aliases = array();
1004
        foreach ($this->handler->value as $value) {
1005
          $join = $this->get_join();
1006
          if ($this->handler->operator == 'and') {
1007
            $join->type = 'INNER';
1008
          }
1009
          $join->extra = array(
1010
            array(
1011
              'field' => $this->handler->real_field,
1012
              'value' => $value,
1013
              'numeric' => !empty($this->handler->definition['numeric']),
1014
            ),
1015
          );
1016

    
1017
          // The table alias needs to be unique to this value across the
1018
          // multiple times the filter or argument is called by the view.
1019
          if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
1020
            if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
1021
              $this->handler->view->many_to_one_count[$this->handler->table] = 0;
1022
            }
1023
            $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
1024
          }
1025
          $alias = $this->handler->table_aliases[$value] = $this->add_table($join, $this->handler->view->many_to_one_aliases[$field][$value]);
1026

    
1027
          // and set table_alias to the first of these.
1028
          if (empty($this->handler->table_alias)) {
1029
            $this->handler->table_alias = $alias;
1030
          }
1031
        }
1032
      }
1033
      // Case 3: it's a 'not'.
1034
      // We just do one join. We'll add a where clause during the query phase
1035
      // to ensure that $table.$field IS NULL.
1036
      else {
1037
        $join = $this->get_join();
1038
        $join->type = 'LEFT';
1039
        $join->extra = array();
1040
        $join->extra_type = 'OR';
1041
        foreach ($this->handler->value as $value) {
1042
          $join->extra[] = array(
1043
            'field' => $this->handler->real_field,
1044
            'value' => $value,
1045
            'numeric' => !empty($this->handler->definition['numeric']),
1046
          );
1047
        }
1048

    
1049
        $this->handler->table_alias = $this->add_table($join);
1050
      }
1051
    }
1052
    return $this->handler->table_alias;
1053
  }
1054

    
1055
  /**
1056
   * Provides a unique placeholders for handlers.
1057
   */
1058
  public function placeholder() {
1059
    return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
1060
  }
1061

    
1062
  /**
1063
   *
1064
   */
1065
  public function add_filter() {
1066
    if (empty($this->handler->value)) {
1067
      return;
1068
    }
1069
    $this->handler->ensure_my_table();
1070

    
1071
    // Shorten some variables.
1072
    $field = $this->get_field();
1073
    $options = $this->handler->options;
1074
    $operator = $this->handler->operator;
1075
    $formula = !empty($this->formula);
1076
    $value = $this->handler->value;
1077
    if (empty($options['group'])) {
1078
      $options['group'] = 0;
1079
    }
1080

    
1081
    // Determine whether a single expression is enough(FALSE) or the conditions
1082
    // should be added via an db_or()/db_and() (TRUE).
1083
    $add_condition = TRUE;
1084
    if ($operator == 'not') {
1085
      $value = NULL;
1086
      $operator = 'IS NULL';
1087
      $add_condition = FALSE;
1088
    }
1089
    elseif ($operator == 'or' && empty($options['reduce_duplicates'])) {
1090
      if (count($value) > 1) {
1091
        $operator = 'IN';
1092
      }
1093
      else {
1094
        $value = is_array($value) ? array_pop($value) : $value;
1095
        $operator = '=';
1096
      }
1097
      $add_condition = FALSE;
1098
    }
1099

    
1100
    if (!$add_condition) {
1101
      if ($formula) {
1102
        $placeholder = $this->placeholder();
1103
        if ($operator == 'IN') {
1104
          $operator = "$operator IN($placeholder)";
1105
        }
1106
        else {
1107
          $operator = "$operator $placeholder";
1108
        }
1109
        $placeholders = array(
1110
          $placeholder => $value,
1111
        ) + $this->placeholders;
1112
        $this->handler->query->add_where_expression($options['group'], "$field $operator", $placeholders);
1113
      }
1114
      else {
1115
        $this->handler->query->add_where($options['group'], $field, $value, $operator);
1116
      }
1117
    }
1118

    
1119
    if ($add_condition) {
1120
      $field = $this->handler->real_field;
1121
      $clause = $operator == 'or' ? db_or() : db_and();
1122
      foreach ($this->handler->table_aliases as $value => $alias) {
1123
        $clause->condition("$alias.$field", $value);
1124
      }
1125

    
1126
      // Implode on either AND or OR.
1127
      $this->handler->query->add_where($options['group'], $clause);
1128
    }
1129
  }
1130

    
1131
}
1132

    
1133
/**
1134
 * Break x,y,z and x+y+z into an array. Works for strings.
1135
 *
1136
 * @param string $str
1137
 *   The string to parse.
1138
 * @param object $object
1139
 *   The object to use as a base. If not specified one will be created.
1140
 *
1141
 * @return object
1142
 *   An object containing
1143
 *   - operator: Either 'and' or 'or'
1144
 *   - value: An array of numeric values.
1145
 */
1146
function views_break_phrase_string($str, &$handler = NULL) {
1147
  if (!$handler) {
1148
    $handler = new stdClass();
1149
  }
1150

    
1151
  // Set up defaults.
1152
  if (!isset($handler->value)) {
1153
    $handler->value = array();
1154
  }
1155

    
1156
  if (!isset($handler->operator)) {
1157
    $handler->operator = 'or';
1158
  }
1159

    
1160
  if ($str == '') {
1161
    return $handler;
1162
  }
1163

    
1164
  // Determine if the string has 'or' operators (plus signs) or 'and' operators
1165
  // (commas) and split the string accordingly. If we have an 'and' operator,
1166
  // spaces are treated as part of the word being split, but otherwise they are
1167
  // treated the same as a plus sign.
1168
  $or_wildcard = '[^\s+,]';
1169
  $and_wildcard = '[^+,]';
1170
  if (preg_match("/^({$or_wildcard}+[+ ])+{$or_wildcard}+$/", $str)) {
1171
    $handler->operator = 'or';
1172
    $handler->value = preg_split('/[+ ]/', $str);
1173
  }
1174
  elseif (preg_match("/^({$and_wildcard}+,)*{$and_wildcard}+$/", $str)) {
1175
    $handler->operator = 'and';
1176
    $handler->value = explode(',', $str);
1177
  }
1178

    
1179
  // Keep an 'error' value if invalid strings were given.
1180
  if (!empty($str) && (empty($handler->value) || !is_array($handler->value))) {
1181
    $handler->value = array(-1);
1182
    return $handler;
1183
  }
1184

    
1185
  // Doubly ensure that all values are strings only.
1186
  foreach ($handler->value as $id => $value) {
1187
    $handler->value[$id] = (string) $value;
1188
  }
1189

    
1190
  return $handler;
1191
}
1192

    
1193
/**
1194
 * Break x,y,z and x+y+z into an array. Numeric only.
1195
 *
1196
 * @param string $str
1197
 *   The string to parse.
1198
 * @param object $handler
1199
 *   The handler object to use as a base. If not specified one will be created.
1200
 *
1201
 * @return $handler
1202
 *   The new handler object.
1203
 */
1204
function views_break_phrase($str, &$handler = NULL) {
1205
  if (!$handler) {
1206
    $handler = new stdClass();
1207
  }
1208

    
1209
  // Set up defaults.
1210
  if (!isset($handler->value)) {
1211
    $handler->value = array();
1212
  }
1213

    
1214
  if (!isset($handler->operator)) {
1215
    $handler->operator = 'or';
1216
  }
1217

    
1218
  if (empty($str)) {
1219
    return $handler;
1220
  }
1221

    
1222
  if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) {
1223
    // The '+' character in a query string may be parsed as ' '.
1224
    $handler->operator = 'or';
1225
    $handler->value = preg_split('/[+ ]/', $str);
1226
  }
1227
  elseif (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) {
1228
    $handler->operator = 'and';
1229
    $handler->value = explode(',', $str);
1230
  }
1231

    
1232
  // Keep an 'error' value if invalid strings were given.
1233
  if (!empty($str) && (empty($handler->value) || !is_array($handler->value))) {
1234
    $handler->value = array(-1);
1235
    return $handler;
1236
  }
1237

    
1238
  // Doubly ensure that all values are numeric only.
1239
  foreach ($handler->value as $id => $value) {
1240
    $handler->value[$id] = intval($value);
1241
  }
1242

    
1243
  return $handler;
1244
}
1245

    
1246
// --------------------------------------------------------------------------
1247
// Date helper functions.
1248
/**
1249
 * Figure out what timezone we're in; needed for some date manipulations.
1250
 */
1251
function views_get_timezone() {
1252
  global $user;
1253
  if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
1254
    $timezone = $user->timezone;
1255
  }
1256
  else {
1257
    $timezone = variable_get('date_default_timezone', 0);
1258
  }
1259

    
1260
  // Set up the database timezone.
1261
  $db_type = Database::getConnection()->databaseType();
1262
  if (in_array($db_type, array('mysql', 'pgsql'))) {
1263
    $offset = '+00:00';
1264
    static $already_set = FALSE;
1265
    if (!$already_set) {
1266
      if ($db_type == 'pgsql') {
1267
        db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
1268
      }
1269
      elseif ($db_type == 'mysql') {
1270
        db_query("SET @@session.time_zone = '$offset'");
1271
      }
1272

    
1273
      $already_set = TRUE;
1274
    }
1275
  }
1276

    
1277
  return $timezone;
1278
}
1279

    
1280
/**
1281
 * Helper function to create cross-database SQL dates.
1282
 *
1283
 * @param string $field
1284
 *   The real table and field name, like 'tablename.fieldname'.
1285
 * @param string $field_type
1286
 *  The type of date field, 'int' or 'datetime'.
1287
 * @param string $set_offset
1288
 *   The name of a field that holds the timezone offset or a fixed timezone
1289
 *   offset value. If not provided, the normal Drupal timezone handling
1290
 *   will be used, i.e. $set_offset = 0 will make no timezone adjustment.
1291
 *
1292
 * @return string
1293
 *   An appropriate SQL string for the db type and field type.
1294
 */
1295
function views_date_sql_field($field, $field_type = 'int', $set_offset = NULL) {
1296
  $db_type = Database::getConnection()->databaseType();
1297
  $offset = $set_offset !== NULL ? $set_offset : views_get_timezone();
1298
  if (isset($offset) && !is_numeric($offset)) {
1299
    $dtz = new DateTimeZone($offset);
1300
    $dt = new DateTime("now", $dtz);
1301
    $offset_seconds = $dtz->getOffset($dt);
1302
  }
1303

    
1304
  switch ($db_type) {
1305
    case 'mysql':
1306
      switch ($field_type) {
1307
        case 'int':
1308
          $field = "DATE_ADD('19700101', INTERVAL $field SECOND)";
1309
          break;
1310

    
1311
        case 'datetime':
1312
          break;
1313
      }
1314
      if (!empty($offset)) {
1315
        $field = "($field + INTERVAL $offset_seconds SECOND)";
1316
      }
1317
      return $field;
1318

    
1319
    case 'pgsql':
1320
      switch ($field_type) {
1321
        case 'int':
1322
          $field = "TO_TIMESTAMP($field)";
1323
          break;
1324

    
1325
        case 'datetime':
1326
          break;
1327
      }
1328
      if (!empty($offset)) {
1329
        $field = "($field + INTERVAL '$offset_seconds SECONDS')";
1330
      }
1331
      return $field;
1332

    
1333
    case 'sqlite':
1334
      if (!empty($offset)) {
1335
        $field = "($field + '$offset_seconds')";
1336
      }
1337
      return $field;
1338
  }
1339
}
1340

    
1341
/**
1342
 * Helper function to create cross-database SQL date formatting.
1343
 *
1344
 * @param string $format
1345
 *   A format string for the result, like 'Y-m-d H:i:s'.
1346
 * @param string $field
1347
 *   The real table and field name, like 'tablename.fieldname'.
1348
 * @param string $field_type
1349
 *   The type of date field, 'int' or 'datetime'.
1350
 * @param string $set_offset
1351
 *   The name of a field that holds the timezone offset or a fixed timezone
1352
 *   offset value. If not provided, the normal Drupal timezone handling will be
1353
 *   used, i.e. $set_offset = 0 will make no timezone adjustment.
1354
 *
1355
 * @return string
1356
 *   An appropriate SQL string for the db type and field type.
1357
 */
1358
function views_date_sql_format($format, $field, $field_type = 'int', $set_offset = NULL) {
1359
  $db_type = Database::getConnection()->databaseType();
1360
  $field = views_date_sql_field($field, $field_type, $set_offset);
1361
  switch ($db_type) {
1362
    case 'mysql':
1363
      $replace = array(
1364
        'Y' => '%Y',
1365
        'y' => '%y',
1366
        'M' => '%b',
1367
        'm' => '%m',
1368
        'n' => '%c',
1369
        'F' => '%M',
1370
        'D' => '%a',
1371
        'd' => '%d',
1372
        'l' => '%W',
1373
        'j' => '%e',
1374
        'W' => '%v',
1375
        'H' => '%H',
1376
        'h' => '%h',
1377
        'i' => '%i',
1378
        's' => '%s',
1379
        'A' => '%p',
1380
      );
1381
      $format = strtr($format, $replace);
1382
      return "DATE_FORMAT($field, '$format')";
1383

    
1384
    case 'pgsql':
1385
      $replace = array(
1386
        'Y' => 'YYYY',
1387
        'y' => 'YY',
1388
        'M' => 'Mon',
1389
        'm' => 'MM',
1390
        // No format for Numeric representation of a month, without leading
1391
        // zeros.
1392
        'n' => 'MM',
1393
        'F' => 'Month',
1394
        'D' => 'Dy',
1395
        'd' => 'DD',
1396
        'l' => 'Day',
1397
        // No format for Day of the month without leading zeros.
1398
        'j' => 'DD',
1399
        'W' => 'WW',
1400
        'H' => 'HH24',
1401
        'h' => 'HH12',
1402
        'i' => 'MI',
1403
        's' => 'SS',
1404
        'A' => 'AM',
1405
      );
1406
      $format = strtr($format, $replace);
1407
      return "TO_CHAR($field, '$format')";
1408

    
1409
    case 'sqlite':
1410
      $replace = array(
1411
        // 4 digit year number.
1412
        'Y' => '%Y',
1413
        // No format for 2 digit year number.
1414
        'y' => '%Y',
1415
        // No format for 3 letter month name.
1416
        'M' => '%m',
1417
        // Month number with leading zeros.
1418
        'm' => '%m',
1419
        // No format for month number without leading zeros.
1420
        'n' => '%m',
1421
        // No format for full month name.
1422
        'F' => '%m',
1423
        // No format for 3 letter day name.
1424
        'D' => '%d',
1425
        // Day of month number with leading zeros.
1426
        'd' => '%d',
1427
        // No format for full day name.
1428
        'l' => '%d',
1429
        // No format for day of month number without leading zeros.
1430
        'j' => '%d',
1431
        // ISO week number.
1432
        'W' => '%W',
1433
        // 24 hour hour with leading zeros.
1434
        'H' => '%H',
1435
        // No format for 12 hour hour with leading zeros.
1436
        'h' => '%H',
1437
        // Minutes with leading zeros.
1438
        'i' => '%M',
1439
        // Seconds with leading zeros.
1440
        's' => '%S',
1441
        // No format for AM/PM.
1442
        'A' => '',
1443
      );
1444
      $format = strtr($format, $replace);
1445
      return "strftime('$format', $field, 'unixepoch')";
1446
  }
1447
}
1448

    
1449
/**
1450
 * Helper function to create cross-database SQL date extraction.
1451
 *
1452
 * @param string $extract_type
1453
 *   The type of value to extract from the date, like 'MONTH'.
1454
 * @param string $field
1455
 *   The real table and field name, like 'tablename.fieldname'.
1456
 * @param string $field_type
1457
 *   The type of date field, 'int' or 'datetime'.
1458
 * @param string $set_offset
1459
 *   The name of a field that holds the timezone offset or a fixed timezone
1460
 *   offset value. If not provided, the normal Drupal timezone handling
1461
 *   will be used, i.e. $set_offset = 0 will make no timezone adjustment.
1462
 *
1463
 * @return string
1464
 *   An appropriate SQL string for the db type and field type.
1465
 */
1466
function views_date_sql_extract($extract_type, $field, $field_type = 'int', $set_offset = NULL) {
1467
  $db_type = Database::getConnection()->databaseType();
1468
  $field = views_date_sql_field($field, $field_type, $set_offset);
1469

    
1470
  // Note there is no space after FROM to avoid db_rewrite problems
1471
  // @see http://drupal.org/node/79904.
1472
  switch ($extract_type) {
1473
    case('DATE'):
1474
      return $field;
1475

    
1476
    case('YEAR'):
1477
      return "EXTRACT(YEAR FROM($field))";
1478

    
1479
    case('MONTH'):
1480
      return "EXTRACT(MONTH FROM($field))";
1481

    
1482
    case('DAY'):
1483
      return "EXTRACT(DAY FROM($field))";
1484

    
1485
    case('HOUR'):
1486
      return "EXTRACT(HOUR FROM($field))";
1487

    
1488
    case('MINUTE'):
1489
      return "EXTRACT(MINUTE FROM($field))";
1490

    
1491
    case('SECOND'):
1492
      return "EXTRACT(SECOND FROM($field))";
1493

    
1494
    case('WEEK'):
1495
      // ISO week number for date.
1496
      switch ($db_type) {
1497
        case('mysql'):
1498
          // WEEK using arg 3 in mysql should return the same value as postgres
1499
          // EXTRACT.
1500
          return "WEEK($field, 3)";
1501

    
1502
        case('pgsql'):
1503
          return "EXTRACT(WEEK FROM($field))";
1504
      }
1505
    case('DOW'):
1506
      switch ($db_type) {
1507
        // MySQL returns 1 for Sunday through 7 for Saturday.
1508
        case('mysql'):
1509
          return "INTEGER(DAYOFWEEK($field) - 1)";
1510

    
1511
        // PHP date functions and postgres use 0 for Sunday and 6 for Saturday.
1512
        case('pgsql'):
1513
          return "EXTRACT(DOW FROM($field))";
1514
      }
1515
    case('DOY'):
1516
      switch ($db_type) {
1517
        case('mysql'):
1518
          return "DAYOFYEAR($field)";
1519

    
1520
        case('pgsql'):
1521
          return "EXTRACT(DOY FROM($field))";
1522
      }
1523
  }
1524
}
1525

    
1526
/**
1527
 * @}
1528
 */
1529

    
1530
/**
1531
 * @defgroup views_join_handlers Views join handlers
1532
 * @{
1533
 * Handlers to tell Views how to join tables together.
1534
 *
1535
 * Here is how you do complex joins:
1536
 *
1537
 * @code
1538
 * class views_join_complex extends views_join {
1539
 *   // PHP 4 doesn't call constructors of the base class automatically from a
1540
 *   // constructor of a derived class. It is your responsibility to propagate
1541
 *   // the call to constructors upstream where appropriate.
1542
 *   public function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') {
1543
 *     parent::construct($table, $left_table, $left_field, $field, $extra, $type);
1544
 *   }
1545
 *
1546
 *   public function build_join($select_query, $table, $view_query) {
1547
 *     $this->extra = 'foo.bar = baz.boing';
1548
 *     parent::build_join($select_query, $table, $view_query);
1549
 *   }
1550
 * }
1551
 * @endcode
1552
 */
1553

    
1554
/**
1555
 * A function class to represent a join and create the SQL necessary
1556
 * to implement the join.
1557
 *
1558
 * This is the Delegation pattern. If we had PHP5 exclusively, we would
1559
 * declare this an interface.
1560
 *
1561
 * Extensions of this class can be used to create more interesting joins.
1562
 *
1563
 * 'join' definition:
1564
 *   - table: table to join (right table)
1565
 *   - field: field to join on (right field)
1566
 *   - left_table: The table we join to
1567
 *   - left_field: The field we join to
1568
 *   - type: either LEFT (default) or INNER
1569
 *   - extra: An array of extra conditions on the join. Each condition is
1570
 *     either a string that's directly added, or an array of items:
1571
 *   - - table: If not set, current table; if NULL, no table. If you specify a
1572
 *       table in cached definition, Views will try to load from an existing
1573
 *       alias. If you use realtime joins, it works better.
1574
 *   - - field: Field or formula
1575
 *       in formulas we can reference the right table by using %alias
1576
 *
1577
 * @see SelectQueryInterface::addJoin()
1578
 *   - operator: Defaults to =.
1579
 *   - value: Must be set. If an array, operator will be defaulted to IN.
1580
 *   - numeric: If true, the value will not be surrounded in quotes.
1581
 *   - extra type: How all the extras will be combined. Either AND or OR.
1582
 *     Defaults to AND.
1583
 */
1584
class views_join {
1585
  public $table = NULL;
1586
  public $left_table = NULL;
1587
  public $left_field = NULL;
1588
  public $field = NULL;
1589
  public $extra = NULL;
1590
  public $type = NULL;
1591
  public $definition = array();
1592

    
1593
  /**
1594
   * Construct the views_join object.
1595
   */
1596
  public function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') {
1597
    $this->extra_type = 'AND';
1598
    if (!empty($table)) {
1599
      $this->table = $table;
1600
      $this->left_table = $left_table;
1601
      $this->left_field = $left_field;
1602
      $this->field = $field;
1603
      $this->extra = $extra;
1604
      $this->type = strtoupper($type);
1605
    }
1606
    elseif (!empty($this->definition)) {
1607
      // If no arguments, construct from definition. These four must exist or
1608
      // it will throw notices.
1609
      $this->table = $this->definition['table'];
1610
      $this->left_table = $this->definition['left_table'];
1611
      $this->left_field = $this->definition['left_field'];
1612
      $this->field = $this->definition['field'];
1613
      if (!empty($this->definition['extra'])) {
1614
        $this->extra = $this->definition['extra'];
1615
      }
1616
      if (!empty($this->definition['extra type'])) {
1617
        $this->extra_type = strtoupper($this->definition['extra type']);
1618
      }
1619

    
1620
      $this->type = !empty($this->definition['type']) ? strtoupper($this->definition['type']) : 'LEFT';
1621
    }
1622
  }
1623

    
1624
  /**
1625
   * Build the SQL for the join this object represents.
1626
   *
1627
   * When possible, try to use table alias instead of table names.
1628
   *
1629
   * @param SelectQueryInterface $select_query
1630
   *   An Implements SelectQueryInterface.
1631
   * @param string $table
1632
   *   The base table to join.
1633
   * @param views_plugin_query $view_query
1634
   *   The source query, Implements views_plugin_query.
1635
   */
1636
  public function build_join($select_query, $table, $view_query) {
1637
    if (empty($this->definition['table formula'])) {
1638
      $right_table = $this->table;
1639
    }
1640
    else {
1641
      $right_table = $this->definition['table formula'];
1642
    }
1643

    
1644
    if ($this->left_table) {
1645
      $left = $view_query->get_table_info($this->left_table);
1646
      $left_field = "$left[alias].$this->left_field";
1647
    }
1648
    else {
1649
      // This can be used if left_field is a formula or something. It should be
1650
      // used only *very* rarely.
1651
      $left_field = $this->left_field;
1652
    }
1653

    
1654
    $condition = "$left_field = $table[alias].$this->field";
1655
    $arguments = array();
1656

    
1657
    // Tack on the extra.
1658
    if (isset($this->extra)) {
1659
      // If extra has been provided as string instead of an array, convert it
1660
      // to an array.
1661
      if (!is_array($this->extra)) {
1662
        $this->extra = array($this->extra);
1663
      }
1664

    
1665
      $extras = array();
1666
      foreach ($this->extra as $info) {
1667
        if (is_array($info)) {
1668
          $extra = '';
1669
          // Figure out the table name. Remember, only use aliases provided if
1670
          // at all possible.
1671
          $join_table = '';
1672
          if (!array_key_exists('table', $info)) {
1673
            $join_table = $table['alias'] . '.';
1674
          }
1675
          elseif (isset($info['table'])) {
1676
            // If we're aware of a table alias for this table, use the table
1677
            // alias instead of the table name.
1678
            if (isset($left) && $left['table'] == $info['table']) {
1679
              $join_table = $left['alias'] . '.';
1680
            }
1681
            else {
1682
              $join_table = $info['table'] . '.';
1683
            }
1684
          }
1685

    
1686
          // Convert a single-valued array of values to the single-value case,
1687
          // and transform from IN() notation to = notation.
1688
          if (is_array($info['value']) && count($info['value']) == 1) {
1689
            if (empty($info['operator'])) {
1690
              $operator = '=';
1691
            }
1692
            else {
1693
              $operator = $info['operator'] == 'NOT IN' ? '!=' : '=';
1694
            }
1695
            $info['value'] = array_shift($info['value']);
1696
          }
1697

    
1698
          if (is_array($info['value'])) {
1699
            // With an array of values, we need multiple placeholders and the
1700
            // 'IN' operator is implicit.
1701
            foreach ($info['value'] as $value) {
1702
              $placeholder_i = $view_query->placeholder('views_join_condition_');
1703
              $arguments[$placeholder_i] = $value;
1704
            }
1705

    
1706
            $operator = !empty($info['operator']) ? $info['operator'] : 'IN';
1707
            $placeholder = '( ' . implode(', ', array_keys($arguments)) . ' )';
1708
          }
1709
          else {
1710
            // With a single value, the '=' operator is implicit.
1711
            $operator = !empty($info['operator']) ? $info['operator'] : '=';
1712
            $placeholder = $view_query->placeholder('views_join_condition_');
1713
            $arguments[$placeholder] = $info['value'];
1714
          }
1715
          $extras[] = "$join_table$info[field] $operator $placeholder";
1716
        }
1717
        elseif (is_string($info)) {
1718
          $extras[] = $info;
1719
        }
1720
      }
1721

    
1722
      if ($extras) {
1723
        if (count($extras) == 1) {
1724
          $condition .= ' AND (' . array_shift($extras) . ')';
1725
        }
1726
        else {
1727
          $condition .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')';
1728
        }
1729
      }
1730
    }
1731

    
1732
    $select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments);
1733
  }
1734
}
1735

    
1736
/**
1737
 * Join handler for relationships that join with a subquery as the left field.
1738
 *
1739
 * For example:
1740
 *  LEFT JOIN node node_term_data ON ([YOUR SUBQUERY HERE]) = node_term_data.nid
1741
 *
1742
 * 'join' definition:
1743
 *   Same as views_join class above, except:
1744
 *   - left_query: The subquery to use in the left side of the join clause.
1745
 */
1746
class views_join_subquery extends views_join {
1747

    
1748
  /**
1749
   * {@inheritdoc}
1750
   */
1751
  public function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') {
1752
    parent::construct($table, $left_table, $left_field, $field, $extra, $type);
1753
    $this->left_query = $this->definition['left_query'];
1754
  }
1755

    
1756
  /**
1757
   * Build the SQL for the join this object represents.
1758
   *
1759
   * @param object $select_query
1760
   *   An Implements SelectQueryInterface.
1761
   * @param string $table
1762
   *   The base table to join.
1763
   * @param array $view_query
1764
   *   The source query, Implements views_plugin_query.
1765
   */
1766
  public function build_join($select_query, $table, $view_query) {
1767
    if (empty($this->definition['table formula'])) {
1768
      $right_table = "{" . $this->table . "}";
1769
    }
1770
    else {
1771
      $right_table = $this->definition['table formula'];
1772
    }
1773

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

    
1778
    // Tack on the extra.
1779
    // This is just copied verbatim from the parent class, which itself has a
1780
    // bug.
1781
    // @see http://drupal.org/node/1118100
1782
    if (isset($this->extra)) {
1783
      // If extra has been provided as string instead of an array, convert it
1784
      // to an array.
1785
      if (!is_array($this->extra)) {
1786
        $this->extra = array($this->extra);
1787
      }
1788

    
1789
      $extras = array();
1790
      foreach ($this->extra as $info) {
1791
        if (is_array($info)) {
1792
          $extra = '';
1793
          // Figure out the table name. Remember, only use aliases provided if
1794
          // at all possible.
1795
          $join_table = '';
1796
          if (!array_key_exists('table', $info)) {
1797
            $join_table = $table['alias'] . '.';
1798
          }
1799
          elseif (isset($info['table'])) {
1800
            $join_table = $info['table'] . '.';
1801
          }
1802

    
1803
          $placeholder = ':views_join_condition_' . $select_query->nextPlaceholder();
1804

    
1805
          if (is_array($info['value'])) {
1806
            $operator = !empty($info['operator']) ? $info['operator'] : 'IN';
1807
            // Transform from IN() notation to = notation if just one value.
1808
            if (count($info['value']) == 1) {
1809
              $info['value'] = array_shift($info['value']);
1810
              $operator = $operator == 'NOT IN' ? '!=' : '=';
1811
            }
1812
          }
1813
          else {
1814
            $operator = !empty($info['operator']) ? $info['operator'] : '=';
1815
          }
1816

    
1817
          $extras[] = "$join_table$info[field] $operator $placeholder";
1818
          $arguments[$placeholder] = $info['value'];
1819
        }
1820
        elseif (is_string($info)) {
1821
          $extras[] = $info;
1822
        }
1823
      }
1824

    
1825
      if ($extras) {
1826
        if (count($extras) == 1) {
1827
          $condition .= ' AND (' . array_shift($extras) . ')';
1828
        }
1829
        else {
1830
          $condition .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')';
1831
        }
1832
      }
1833
    }
1834

    
1835
    $select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments);
1836
  }
1837
}
1838

    
1839
/**
1840
 * @}
1841
 */