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 @ 4003efde

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
    if (empty($this->definition['group'])) {
282
      return $title;
283
    }
284
    return t('!group: !title', array('!group' => $this->definition['group'], '!title' => $title));
285
  }
286

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

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

    
316
    return $field;
317
  }
318

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

    
336
      case 'xss_admin':
337
        $value = filter_xss_admin($value);
338
        break;
339

    
340
      case 'url':
341
        $value = check_url($value);
342
        break;
343

    
344
      default:
345
        $value = check_plain($value);
346
        break;
347
    }
348
    return $value;
349
  }
350

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

    
369
    switch ($option) {
370
      default:
371
        return $string;
372
      case 'upper':
373
        return drupal_strtoupper($string);
374

    
375
      case 'lower':
376
        return drupal_strtolower($string);
377

    
378
      case 'ucfirst':
379
        return drupal_strtoupper(drupal_substr($string, 0, 1)) . drupal_substr($string, 1);
380

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

    
391
  /**
392
   * Validate the options form.
393
   */
394
  public function options_validate(&$form, &$form_state) {
395
  }
396

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

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

    
415
    // This form is long and messy enough that the "Administrative title" option
416
    // belongs in a "more options" fieldset at the bottom of the form.
417
    $form['more'] = array(
418
      '#type' => 'fieldset',
419
      '#title' => t('More'),
420
      '#collapsible' => TRUE,
421
      '#collapsed' => TRUE,
422
      '#weight' => 150,
423
    );
424

    
425
    // Allow to alter the default values brought into the form.
426
    // Triggers hook_views_handler_options_alter().
427
    drupal_alter('views_handler_options', $this->options, $this);
428
  }
429

    
430
  /**
431
   * Perform any necessary changes to the form values prior to storage.
432
   * There is no need for this function to actually store the data.
433
   */
434
  public function options_submit(&$form, &$form_state) {
435
  }
436

    
437
  /**
438
   * Provides the handler some groupby.
439
   */
440
  public function use_group_by() {
441
    return TRUE;
442
  }
443

    
444
  /**
445
   * Provide a form for aggregation settings.
446
   */
447
  public function groupby_form(&$form, &$form_state) {
448
    $view = &$form_state['view'];
449
    $display_id = $form_state['display_id'];
450
    $types = views_object_types();
451
    $type = $form_state['type'];
452
    $id = $form_state['id'];
453

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

    
457
    $form['#section'] = $display_id . '-' . $type . '-' . $id;
458

    
459
    $view->init_query();
460
    $info = $view->query->get_aggregation_info();
461
    foreach ($info as $id => $aggregate) {
462
      $group_types[$id] = $aggregate['title'];
463
    }
464

    
465
    $form['group_type'] = array(
466
      '#type' => 'select',
467
      '#title' => t('Aggregation type'),
468
      '#default_value' => $this->options['group_type'],
469
      '#description' => t('Select the aggregation function to use on this field.'),
470
      '#options' => $group_types,
471
    );
472
  }
473

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

    
481
    $item['group_type'] = $form_state['values']['options']['group_type'];
482
  }
483

    
484
  /**
485
   * If a handler has 'extra options' it will get a little settings widget and
486
   * another form called extra_options.
487
   */
488
  public function has_extra_options() {
489
    return FALSE;
490
  }
491

    
492
  /**
493
   * Provide defaults for the handler.
494
   */
495
  public function extra_options(&$option) {
496
  }
497

    
498
  /**
499
   * Provide a form for setting options.
500
   */
501
  public function extra_options_form(&$form, &$form_state) {
502
  }
503

    
504
  /**
505
   * Validate the options form.
506
   */
507
  public function extra_options_validate($form, &$form_state) {
508
  }
509

    
510
  /**
511
   * Perform any necessary changes to the form values prior to storage.
512
   * There is no need for this function to actually store the data.
513
   */
514
  public function extra_options_submit($form, &$form_state) {
515
  }
516

    
517
  /**
518
   * Determine if a handler can be exposed.
519
   */
520
  public function can_expose() {
521
    return FALSE;
522
  }
523

    
524
  /**
525
   * Set new exposed option defaults when exposed setting is flipped
526
   * on.
527
   */
528
  public function expose_options() {
529
  }
530

    
531
  /**
532
   * Get information about the exposed form for the form renderer.
533
   */
534
  public function exposed_info() {
535
  }
536

    
537
  /**
538
   * Render our chunk of the exposed handler form when selecting.
539
   */
540
  public function exposed_form(&$form, &$form_state) {
541
  }
542

    
543
  /**
544
   * Validate the exposed handler form.
545
   */
546
  public function exposed_validate(&$form, &$form_state) {
547
  }
548

    
549
  /**
550
   * Submit the exposed handler form.
551
   */
552
  public function exposed_submit(&$form, &$form_state) {
553
  }
554

    
555
  /**
556
   * Form for exposed handler options.
557
   */
558
  public function expose_form(&$form, &$form_state) {
559
  }
560

    
561
  /**
562
   * Validate the options form.
563
   */
564
  public function expose_validate($form, &$form_state) {
565
  }
566

    
567
  /**
568
   * Perform any necessary changes to the form exposes prior to storage.
569
   * There is no need for this function to actually store the data.
570
   */
571
  public function expose_submit($form, &$form_state) {
572
  }
573

    
574
  /**
575
   * Shortcut to display the expose/hide button.
576
   */
577
  public function show_expose_button(&$form, &$form_state) {
578
  }
579

    
580
  /**
581
   * Shortcut to display the exposed options form.
582
   */
583
  public function show_expose_form(&$form, &$form_state) {
584
    if (empty($this->options['exposed'])) {
585
      return;
586
    }
587

    
588
    $this->expose_form($form, $form_state);
589

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

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

    
616
    return TRUE;
617
  }
618

    
619
  /**
620
   * Run before the view is built.
621
   *
622
   * This gives all the handlers some time to set up before any handler has
623
   * been fully run.
624
   */
625
  public function pre_query() {
626
  }
627

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

    
638
  /**
639
   * Provides a unique placeholders for handlers.
640
   */
641
  public function placeholder() {
642
    return $this->query->placeholder($this->options['table'] . '_' . $this->options['field']);
643
  }
644

    
645
  /**
646
   * Called just prior to query(), this lets a handler set up any relationship
647
   * it needs.
648
   */
649
  public function set_relationship() {
650
    // Ensure this gets set to something.
651
    $this->relationship = NULL;
652

    
653
    // Don't process non-existant relationships.
654
    if (empty($this->options['relationship']) || $this->options['relationship'] == 'none') {
655
      return;
656
    }
657

    
658
    $relationship = $this->options['relationship'];
659

    
660
    // Ignore missing/broken relationships.
661
    if (empty($this->view->relationship[$relationship])) {
662
      return;
663
    }
664

    
665
    // Check to see if the relationship has already processed. If not, then we
666
    // cannot process it.
667
    if (empty($this->view->relationship[$relationship]->alias)) {
668
      return;
669
    }
670

    
671
    // Finally!
672
    $this->relationship = $this->view->relationship[$relationship]->alias;
673
  }
674

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

    
690
  /**
691
   * Provide text for the administrative summary.
692
   */
693
  public function admin_summary() {
694
  }
695

    
696
  /**
697
   * Determine if the argument needs a style plugin.
698
   *
699
   * @return bool
700
   */
701
  public function needs_style_plugin() {
702
    return FALSE;
703
  }
704

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

    
715
  /**
716
   * Returns TRUE if the exposed filter works like a grouped filter.
717
   */
718
  public function is_a_group() {
719
    return FALSE;
720
  }
721

    
722
  /**
723
   * Define if the exposed input has to be submitted multiple times.
724
   * This is TRUE when exposed filters grouped are using checkboxes as
725
   * widgets.
726
   */
727
  public function multiple_exposed_input() {
728
    return FALSE;
729
  }
730

    
731
  /**
732
   * Take input from exposed handlers and assign to this handler, if necessary.
733
   */
734
  public function accept_exposed_input($input) {
735
    return TRUE;
736
  }
737

    
738
  /**
739
   * If set to remember exposed input in the session, store it there.
740
   */
741
  public function store_exposed_input($input, $status) {
742
    return TRUE;
743
  }
744

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

    
762
    $join = views_get_table_join($this->table, $base_table);
763
    if ($join) {
764
      return clone $join;
765
    }
766
  }
767

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

    
784
  /**
785
   * Determine if the handler is considered 'broken'.
786
   *
787
   * Generally only returns TRUE if the handler can't be found.
788
   *
789
   * @return bool
790
   *   The handler could not be loaded.
791
   */
792
  public function broken() {
793
  }
794

    
795
}
796

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

    
812
  /**
813
   * Contains possible existing placeholders used by the query.
814
   *
815
   * @var array
816
   */
817
  public $placeholders = array();
818

    
819
  /**
820
   * {@inheritdoc}
821
   */
822
  public function __construct(&$handler) {
823
    $this->handler = &$handler;
824
  }
825

    
826
  /**
827
   * {@inheritdoc}
828
   */
829
  static function option_definition(&$options) {
830
    $options['reduce_duplicates'] = array('default' => FALSE, 'bool' => TRUE);
831
  }
832

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

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

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

    
873
    if (empty($join)) {
874
      $join = $this->get_join();
875
    }
876

    
877
    // See if there's a chain between us and the base relationship. If so, we
878
    // need to create a new relationship to use.
879
    $relationship = $this->handler->relationship;
880

    
881
    // Determine the primary table to seek.
882
    if (empty($this->handler->query->relationships[$relationship])) {
883
      $base_table = $this->handler->query->base_table;
884
    }
885
    else {
886
      $base_table = $this->handler->query->relationships[$relationship]['base'];
887
    }
888

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

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

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

    
912
    return $alias;
913
  }
914

    
915
  /**
916
   * {@inheritdoc}
917
   */
918
  public function get_join() {
919
    return $this->handler->get_join();
920
  }
921

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

    
932
    // Shortcuts.
933
    $options = $this->handler->options;
934
    $view = &$this->handler->view;
935
    $query = &$this->handler->query;
936

    
937
    if (!empty($options['require_value'])) {
938
      $join->type = 'INNER';
939
    }
940

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

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

    
998
          $this->handler->table_alias = $this->add_table($join);
999
        }
1000

    
1001
        return $this->handler->table_alias;
1002
      }
1003

    
1004
      // Case 2: it's anything but an 'or'.
1005
      // We do one join per selected value.
1006

    
1007
      // Clone the join for each table:
1008
      $this->handler->table_aliases = array();
1009
      $values = $this->handler->operator === 'not' ? array($this->handler->value) : $this->handler->value;
1010
      foreach ($values as $value) {
1011
        $join = $this->get_join();
1012
        if ($this->handler->operator == 'and') {
1013
          $join->type = 'INNER';
1014
        }
1015
        if (empty($join->extra)) {
1016
          $join->extra = array();
1017
        }
1018
        $join->extra[] = array(
1019
          'field' => $this->handler->real_field,
1020
          'value' => $value,
1021
          'numeric' => !empty($this->handler->definition['numeric']),
1022
        );
1023
        if (($this->handler->is_a_group() && is_array($value)) || $this->handler->operator === 'not') {
1024
          $value = serialize($value);
1025
        }
1026
        // The table alias needs to be unique to this value across the
1027
        // multiple times the filter or argument is called by the view.
1028
        if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
1029
          if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
1030
            $this->handler->view->many_to_one_count[$this->handler->table] = 0;
1031
          }
1032
          $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
1033
          $alias = $this->handler->table_aliases[$value] = $this->add_table($join,
1034
            $this->handler->view->many_to_one_aliases[$field][$value]);
1035
          // and set table_alias to the first of these.
1036
          if (empty($this->handler->table_alias)) {
1037
            $this->handler->table_alias = $alias;
1038
          }
1039
        }
1040
        else {
1041
          $this->handler->table_aliases[$value] = $this->handler->view->many_to_one_aliases[$field][$value];
1042
        }
1043
      }
1044
    }
1045
    return $this->handler->table_alias;
1046
  }
1047

    
1048
  /**
1049
   * Provides a unique placeholders for handlers.
1050
   */
1051
  public function placeholder() {
1052
    return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
1053
  }
1054

    
1055
  /**
1056
   *
1057
   */
1058
  public function add_filter() {
1059
    if (empty($this->handler->value)) {
1060
      return;
1061
    }
1062
    $this->handler->ensure_my_table();
1063

    
1064
    // Shorten some variables.
1065
    $field = $this->get_field();
1066
    $options = $this->handler->options;
1067
    $operator = $this->handler->operator;
1068
    $formula = !empty($this->formula);
1069
    $value = $this->handler->value;
1070
    if (empty($options['group'])) {
1071
      $options['group'] = 0;
1072
    }
1073

    
1074
    // Determine whether a single expression is enough(FALSE) or the conditions
1075
    // should be added via an db_or()/db_and() (TRUE).
1076
    $add_condition = TRUE;
1077
    if ($operator == 'or' && empty($options['reduce_duplicates'])) {
1078
      if (count($value) > 1) {
1079
        $operator = 'IN';
1080
      }
1081
      else {
1082
        $value = is_array($value) ? array_pop($value) : $value;
1083
        if (is_array($value) && count($value) > 1) {
1084
          $operator = 'IN';
1085
        }
1086
        else {
1087
          $operator = '=';
1088
        }
1089
      }
1090
      $add_condition = FALSE;
1091
    }
1092

    
1093
    if (!$add_condition) {
1094
      if ($formula) {
1095
        $placeholder = $this->placeholder();
1096
        if ($operator == 'IN') {
1097
          $operator = "$operator IN($placeholder)";
1098
        }
1099
        else {
1100
          $operator = "$operator $placeholder";
1101
        }
1102
        $placeholders = array(
1103
          $placeholder => $value,
1104
        ) + $this->placeholders;
1105
        $this->handler->query->add_where_expression($options['group'], "$field $operator", $placeholders);
1106
      }
1107
      else {
1108
        $this->handler->query->add_where($options['group'], $field, $value, $operator);
1109
      }
1110
    }
1111

    
1112
    if ($add_condition) {
1113
      $field = $this->handler->real_field;
1114
      $clause = $operator == 'or' ? db_or() : db_and();
1115
      foreach ($this->handler->table_aliases as $value => $alias) {
1116
        if ($operator == 'not') {
1117
          $value = NULL;
1118
        }
1119
        $clause->condition("$alias.$field", $value);
1120
      }
1121

    
1122
      // Implode on either AND or OR.
1123
      $this->handler->query->add_where($options['group'], $clause);
1124
    }
1125
  }
1126

    
1127
}
1128

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

    
1147
  // Set up defaults.
1148
  if (!isset($handler->value)) {
1149
    $handler->value = array();
1150
  }
1151

    
1152
  if (!isset($handler->operator)) {
1153
    $handler->operator = 'or';
1154
  }
1155

    
1156
  if ($str == '') {
1157
    return $handler;
1158
  }
1159

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

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

    
1181
  // Doubly ensure that all values are strings only.
1182
  foreach ($handler->value as $id => $value) {
1183
    $handler->value[$id] = (string) $value;
1184
  }
1185

    
1186
  return $handler;
1187
}
1188

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

    
1205
  // Set up defaults.
1206
  if (!isset($handler->value)) {
1207
    $handler->value = array();
1208
  }
1209

    
1210
  if (!isset($handler->operator)) {
1211
    $handler->operator = 'or';
1212
  }
1213

    
1214
  if (empty($str)) {
1215
    return $handler;
1216
  }
1217

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

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

    
1234
  // Doubly ensure that all values are numeric only.
1235
  foreach ($handler->value as $id => $value) {
1236
    $handler->value[$id] = intval($value);
1237
  }
1238

    
1239
  return $handler;
1240
}
1241

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

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

    
1269
      $already_set = TRUE;
1270
    }
1271
  }
1272

    
1273
  return $timezone;
1274
}
1275

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

    
1300
  switch ($db_type) {
1301
    case 'mysql':
1302
      switch ($field_type) {
1303
        case 'int':
1304
          $field = "DATE_ADD('19700101', INTERVAL $field SECOND)";
1305
          break;
1306

    
1307
        case 'datetime':
1308
          break;
1309
      }
1310
      if (!empty($offset)) {
1311
        $field = "($field + INTERVAL $offset_seconds SECOND)";
1312
      }
1313
      return $field;
1314

    
1315
    case 'pgsql':
1316
      switch ($field_type) {
1317
        case 'int':
1318
          $field = "TO_TIMESTAMP($field)";
1319
          break;
1320

    
1321
        case 'datetime':
1322
          break;
1323
      }
1324
      if (!empty($offset)) {
1325
        $field = "($field + INTERVAL '$offset_seconds SECONDS')";
1326
      }
1327
      return $field;
1328

    
1329
    case 'sqlite':
1330
      if (!empty($offset)) {
1331
        $field = "($field + '$offset_seconds')";
1332
      }
1333
      return $field;
1334
  }
1335
}
1336

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

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

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

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

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

    
1472
    case('YEAR'):
1473
      return "EXTRACT(YEAR FROM($field))";
1474

    
1475
    case('MONTH'):
1476
      return "EXTRACT(MONTH FROM($field))";
1477

    
1478
    case('DAY'):
1479
      return "EXTRACT(DAY FROM($field))";
1480

    
1481
    case('HOUR'):
1482
      return "EXTRACT(HOUR FROM($field))";
1483

    
1484
    case('MINUTE'):
1485
      return "EXTRACT(MINUTE FROM($field))";
1486

    
1487
    case('SECOND'):
1488
      return "EXTRACT(SECOND FROM($field))";
1489

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

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

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

    
1516
        case('pgsql'):
1517
          return "EXTRACT(DOY FROM($field))";
1518
      }
1519
  }
1520
}
1521

    
1522
/**
1523
 * @}
1524
 */
1525

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

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

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

    
1616
      $this->type = !empty($this->definition['type']) ? strtoupper($this->definition['type']) : 'LEFT';
1617
    }
1618
  }
1619

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

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

    
1650
    $condition = "$left_field = $table[alias].$this->field";
1651
    $arguments = array();
1652

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

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

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

    
1694
          if (is_array($info['value'])) {
1695
            $value_placeholders = array();
1696

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1838
/**
1839
 * @}
1840
 */