Projet

Général

Profil

Paste
Télécharger (35,6 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / views_data_export / plugins / views_data_export_plugin_display_export.inc @ 74f6bef0

1
<?php
2

    
3
/**
4
 * @file
5
 * Contains the bulk export display plugin.
6
 *
7
 * This allows views to be rendered in parts by batch API.
8
 */
9

    
10
/**
11
 * The plugin that batches its rendering.
12
 *
13
 * We are based on a feed display for compatibility.
14
 *
15
 * @ingroup views_display_plugins
16
 */
17
class views_data_export_plugin_display_export extends views_plugin_display_feed {
18

    
19
  /**
20
   * The batched execution state of the view.
21
   */
22
  public $batched_execution_state;
23

    
24
  /**
25
   * The alias of the weight field in the index table.
26
   */
27
  var $weight_field_alias = '';
28

    
29
  /**
30
   * A map of the index column names to the expected views aliases.
31
   */
32
  var $field_aliases = array();
33

    
34
  /**
35
   * Private variable that stores the filename to save the results to.
36
   */
37
  var $_output_file = '';
38

    
39
  var $views_data_export_cached_view_loaded;
40

    
41
  var $errors = array();
42

    
43
  /**
44
   * Return the type of styles we require.
45
   */
46
  function get_style_type() { return 'data_export'; }
47

    
48
  /**
49
   * Return the sections that can be defaultable.
50
   */
51
  function defaultable_sections($section = NULL) {
52
    if (in_array($section, array('items_per_page', 'offset', 'use_pager', 'pager_element',))) {
53
      return FALSE;
54
    }
55

    
56
    return parent::defaultable_sections($section);
57
  }
58

    
59
  /**
60
   * Define the option for this view.
61
   */
62
  function option_definition() {
63
    $options = parent::option_definition();
64
    $options['use_batch'] = array('default' => 'no_batch');
65
    $options['items_per_page'] = array('default' => '0');
66
    $options['return_path'] = array('default' => '');
67
    $options['style_plugin']['default'] = 'views_data_export_csv';
68

    
69
    // This is the default size of a segment when doing a batched export.
70
    $options['segment_size']['default'] = 100;
71

    
72
    if (isset($options['defaults']['default']['items_per_page'])) {
73
      $options['defaults']['default']['items_per_page'] = FALSE;
74
    }
75

    
76
    return $options;
77
  }
78

    
79
  /**
80
   * Provide the summary for page options in the views UI.
81
   *
82
   * This output is returned as an array.
83
   */
84
  function options_summary(&$categories, &$options) {
85
    // It is very important to call the parent function here:
86
    parent::options_summary($categories, $options);
87

    
88
    $categories['page']['title'] = t('Data export settings');
89

    
90
    $options['use_batch'] = array(
91
      'category' => 'page',
92
      'title' => t('Batched export'),
93
      'value' => ($this->get_option('use_batch') == 'batch' ? t('Yes') : t('No')),
94
    );
95

    
96
    if (!$this->is_compatible() && $this->get_option('use_batch')) {
97
      $options['use_batch']['value'] .= ' <strong>' . t('(Warning: incompatible)') . '</strong>';
98
    }
99
  }
100

    
101
  /**
102
   * Provide the default form for setting options.
103
   */
104
  function options_form(&$form, &$form_state) {
105
    // It is very important to call the parent function here:
106
    parent::options_form($form, $form_state);
107

    
108
    switch ($form_state['section']) {
109
      case 'use_batch':
110
        $form['#title'] .= t('Batched export');
111
        $form['use_batch'] = array(
112
          '#type' => 'radios',
113
          '#description' => t(''),
114
          '#default_value' => $this->get_option('use_batch'),
115
          '#options' => array(
116
            'no_batch' => t('Export data all in one segment. Possible time and memory limit issues.'),
117
            'batch' => t('Export data in small segments to build a complete export. Recommended for large exports sets (1000+ rows)'),
118
          ),
119
        );
120
        // Allow the administrator to configure the number of items exported per batch.
121
        $form['segment_size'] = array(
122
          '#type' => 'select',
123
          '#title' => t('Segment size'),
124
          '#description' => t('If each row of your export consumes a lot of memory to render, then reduce this value. Higher values will generally mean that the export completes in less time but will have a higher peak memory usage.'),
125
          '#options' => drupal_map_assoc(range(1, 500)),
126
          '#default_value' => $this->get_option('segment_size'),
127
          '#process' => array('ctools_dependent_process'),
128
          '#dependency' => array(
129
            'radio:use_batch' => array('batch')
130
          ),
131
        );
132
        $form['return_path'] = array(
133
          '#title' => t('Return path'),
134
          '#type' => 'textfield',
135
          '#description' => t('Return path after the batched operation, leave empty for default. This path will only be used if the export URL is visited directly, and not by following a link when attached to another view display.'),
136
          '#default_value' => $this->get_option('return_path'),
137
          '#dependency' => array(
138
            'radio:use_batch' => array('batch')
139
          ),
140
        );
141
        if (!$this->is_compatible()) {
142
          $form['use_batch']['#disabled'] = TRUE;
143
          $form['use_batch']['#default_value'] = 'no_batch';
144
          $form['use_batch']['message'] = array (
145
            '#type' => 'markup',
146
            '#markup' => theme('views_data_export_message', array('message' => t('The underlying database (!db_driver) is incompatible with the batched export option and it has been disabled.', array('!db_driver' => $this->_get_database_driver())), 'type' => 'warning')),
147
            '#weight' => -10,
148
          );
149
        }
150
        break;
151

    
152
      case 'cache':
153
        // We're basically going to disable using cache plugins, by disabling
154
        // the UI.
155
        if (isset($form['cache']['type']['#options'])) {
156
          foreach ($form['cache']['type']['#options'] as $id => $v) {
157
            if ($id != 'none') {
158
              unset($form['cache']['type']['#options'][$id]);
159
            }
160
            $form['cache']['type']['#description'] = t("Views data export isn't currently compatible with caching plugins.");
161
          }
162
        }
163
        break;
164

    
165
    }
166
  }
167

    
168
  function get_option($option) {
169
    // Force people to never use caching with Views data export. Sorry folks,
170
    // but it causes too many issues for our workflow. If you really want to add
171
    // caching back, then you can subclass this display handler and override
172
    // this method to add it back.
173
    if ($option == 'cache') {
174
      return array('type' => 'none');
175
    }
176

    
177
    return parent::get_option($option);
178
  }
179

    
180
  /**
181
   * Save the options from the options form.
182
   */
183
  function options_submit(&$form, &$form_state) {
184
    // It is very important to call the parent function here:
185
    parent::options_submit($form, $form_state);
186
    switch ($form_state['section']) {
187
      case 'use_batch':
188
        $this->set_option('use_batch', $form_state['values']['use_batch']);
189
        $this->set_option('segment_size', $form_state['values']['segment_size']);
190
        $this->set_option('return_path', $form_state['values']['return_path']);
191
        break;
192
    }
193
  }
194

    
195
  /**
196
   * Determine if this view should run as a batch or not.
197
   */
198
  function is_batched() {
199
    // The source of this option may change in the future.
200
    return ($this->get_option('use_batch') == 'batch') && empty($this->view->live_preview);
201
  }
202

    
203
  /**
204
   * Add HTTP headers for the file export.
205
   */
206
  function add_http_headers() {
207
    // Ask the style plugin to add any HTTP headers if it wants.
208
    if (method_exists($this->view->style_plugin, 'add_http_headers')) {
209
      $this->view->style_plugin->add_http_headers();
210
    }
211
  }
212

    
213
  /**
214
   * Execute this display handler.
215
   *
216
   * This is the main entry point for this display. We do different things based
217
   * on the stage in the rendering process.
218
   *
219
   * If we are being called for the very first time, the user has usually just
220
   * followed a link to our view. For this phase we:
221
   * - Register a new batched export with our parent module.
222
   * - Build and execute the view, redirecting the output into a temporary table.
223
   * - Set up the batch.
224
   *
225
   * If we are being called during batch processing we:
226
   * - Set up our variables from the context into the display.
227
   * - Call the rendering layer.
228
   * - Return with the appropriate progress value for the batch.
229
   *
230
   * If we are being called after the batch has completed we:
231
   * - Remove the index table.
232
   * - Show the complete page with a download link.
233
   * - Transfer the file if the download link was clicked.
234
   */
235
  function execute() {
236
    if (!$this->is_batched()) {
237
      return parent::execute();
238
    }
239

    
240
    // Try and get a batch context if possible.
241
    $eid = !empty($_GET['eid']) ? $_GET['eid'] :
242
            (!empty($this->batched_execution_state->eid) ? $this->batched_execution_state->eid : FALSE);
243
    if ($eid) {
244
      $this->batched_execution_state = views_data_export_get($eid);
245
    }
246

    
247
    // First time through
248
    if (empty($this->batched_execution_state)) {
249
      $output = $this->execute_initial();
250
    }
251

    
252
    // Call me on the cached version of this view please
253
    // This allows this view to be programatically executed with nothing
254
    // more than the eid in $_GET in order for it to execute the next chunk
255
    // TODO: What is going on here?
256
    /*
257
     Jamsilver tells me this might be useful one day.
258
    if (!$this->views_data_export_cached_view_loaded) {
259
      $view = views_data_export_view_retrieve($this->batched_execution_state->eid);
260
      $view->set_display($this->view->current_display);
261
      $view->display_handler->batched_execution_state->eid = $this->batched_execution_state->eid;
262
      $view->display_handler->views_data_export_cached_view_loaded = TRUE;
263
      $ret =  $view->execute_display($this->view->current_display);
264
      $this->batched_execution_state = &$view->display_handler->batched_execution_state;
265
      return $ret;
266
    }*/
267

    
268
    // Last time through
269
    if ($this->batched_execution_state->batch_state == VIEWS_DATA_EXPORT_FINISHED) {
270
      $output = $this->execute_final();
271
    }
272
    // In the middle of processing
273
    else {
274
      $output = $this->execute_normal();
275
    }
276

    
277
    //Ensure any changes we made to the database sandbox are saved
278
    views_data_export_update($this->batched_execution_state);
279

    
280
    return $output;
281
  }
282

    
283

    
284
  /**
285
   * Initializes the whole export process and starts off the batch process.
286
   *
287
   * Page execution will be ended at the end of this function.
288
   */
289
  function execute_initial() {
290

    
291
    // Register this export with our central table - get a unique eid
292
    // Also store our view in a cache to be retrieved with each batch call
293
    $this->batched_execution_state = views_data_export_new($this->view->name, $this->view->current_display, $this->outputfile_create());
294
    views_data_export_view_store($this->batched_execution_state->eid, $this->view);
295

    
296
    // We need to build the index right now, before we lose $_GET etc.
297
    $this->initialize_index();
298
    //$this->batched_execution_state->fid = $this->outputfile_create();
299

    
300
    // Initialize the progress counter
301
    $this->batched_execution_state->sandbox['max'] = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
302
    // Record the time we started.
303
    list($usec, $sec) = explode(' ', microtime());
304
    $this->batched_execution_state->sandbox['started'] = (float) $usec + (float) $sec;
305

    
306
    // Build up our querystring for the final page callback.
307
    $querystring = array(
308
      'eid' => $this->batched_execution_state->eid,
309
    );
310
    // If we were attached to another view, grab that for the final URL.
311
    if (!empty($_GET['attach']) && isset($this->view->display[$_GET['attach']])) {
312
      // Get the path of the attached display:
313
      $querystring['return-url'] = $this->view->get_url(NULL, $this->view->display[$_GET['attach']]->handler->get_path());
314
    }
315
    else {
316
      $return_path = $this->get_option('return_path');
317
      $querystring['return-url'] = isset($return_path) ? $return_path : NULL;
318
    }
319

    
320
    //Set the batch off
321
    $batch = array(
322
      'operations' => array (
323
        array('_views_data_export_batch_process', array($this->batched_execution_state->eid, $this->view->current_display,  $this->view->get_exposed_input())),
324
      ),
325
      'title' => t('Building export'),
326
      'init_message' => t('Export is starting up.'),
327
      'progress_message' => t('Exporting @percentage% complete,'),
328
      'error_message' => t('Export has encountered an error.'),
329
    );
330

    
331
    // We do not return, so update database sandbox now
332
    views_data_export_update($this->batched_execution_state);
333

    
334
    $final_destination = $this->view->get_url();
335

    
336
    // Provide a way in for others at this point
337
    // e.g. Drush to grab this batch and yet execute it in
338
    // it's own special way
339
    $batch['view_name'] = $this->view->name;
340
    $batch['exposed_filters'] = $this->view->get_exposed_input();
341
    $batch['display_id'] = $this->view->current_display;
342
    $batch['eid'] = $this->batched_execution_state->eid;
343
    $batch_redirect = array($final_destination, array('query' => $querystring));
344
    drupal_alter('views_data_export_batch', $batch, $batch_redirect);
345

    
346
    // Modules may have cleared out $batch, indicating that we shouldn't process further.
347
    if (!empty($batch)) {
348
      batch_set($batch);
349
      // batch_process exits
350
      batch_process($batch_redirect);
351
    }
352
  }
353

    
354

    
355
  /**
356
   * Compiles the next chunk of the output file
357
   */
358
  function execute_normal() {
359

    
360
    // Pass through to our render method,
361
    $output = $this->view->render();
362

    
363
    // Append what was rendered to the output file.
364
    $this->outputfile_write($output);
365

    
366
    // Store for convenience.
367
    $state = &$this->batched_execution_state;
368
    $sandbox = &$state->sandbox;
369

    
370
    // Update progress measurements & move our state forward
371
    switch ($state->batch_state) {
372

    
373
      case VIEWS_DATA_EXPORT_BODY:
374
        // Remove rendered results from our index
375
        if (count($this->view->result) && ($sandbox['weight_field_alias'])) {
376
          $last = end($this->view->result);
377
          db_delete($this->index_tablename())
378
            ->condition($sandbox['weight_field_alias'], $last->{$sandbox['weight_field_alias']}, '<=')
379
            ->execute();
380

    
381
          // Update progress.
382
          $progress = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
383
          // TODO: These next few lines are messy, clean them up.
384
          $progress = 0.99 - ($progress / $sandbox['max'] * 0.99);
385
          $progress = ((int)floor($progress * 100000));
386
          $progress = $progress / 100000;
387
          $sandbox['finished'] = $progress;
388
        }
389
        else {
390
          // No more results.
391
          $progress = 0.99;
392
          $state->batch_state = VIEWS_DATA_EXPORT_FOOTER;
393
        }
394
        break;
395

    
396
      case VIEWS_DATA_EXPORT_HEADER:
397
        $sandbox['finished'] = 0;
398
        $state->batch_state = VIEWS_DATA_EXPORT_BODY;
399
        break;
400

    
401
      case VIEWS_DATA_EXPORT_FOOTER:
402
        $sandbox['finished'] = 1;
403
        $state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
404
        break;
405
    }
406

    
407
    // Create a more helpful exporting message.
408
    $sandbox['message'] = $this->compute_time_remaining($sandbox['started'], $sandbox['finished']);
409
  }
410

    
411

    
412
  /**
413
   * Renders the final page
414
   *  We should be free of the batch at this point
415
   */
416
  function execute_final() {
417
    // Should we download the file.
418
    if (!empty($_GET['download'])) {
419
      // This next method will exit.
420
      $this->transfer_file();
421
    }
422
    else {
423
      // Remove the index table.
424
      $this->remove_index();
425
      return $this->render_complete();
426
    }
427
  }
428

    
429

    
430
  /**
431
   * Render the display.
432
   *
433
   * We basically just work out if we should be rendering the header, body or
434
   * footer and call the appropriate functions on the style plugins.
435
   */
436
  function render() {
437

    
438
    if (!$this->is_batched()) {
439
      $result = parent::render();
440
      if (empty($this->view->live_preview)) {
441
        $this->add_http_headers();
442
      }
443
      return $result;
444
    }
445

    
446
    $this->view->build();
447

    
448
    switch ($this->batched_execution_state->batch_state) {
449
      case VIEWS_DATA_EXPORT_BODY:
450
        $output = $this->view->style_plugin->render_body();
451
        break;
452
      case VIEWS_DATA_EXPORT_HEADER:
453
        $output = $this->view->style_plugin->render_header();
454
        break;
455
      case VIEWS_DATA_EXPORT_FOOTER:
456
        $output = $this->view->style_plugin->render_footer();
457
        break;
458
    }
459

    
460
    return $output;
461
  }
462

    
463

    
464

    
465
  /**
466
   * Trick views into thinking that we have executed the query and got results.
467
   *
468
   * We are called in the build phase of the view, but short circuit straight to
469
   * getting the results and making the view think it has already executed the
470
   * query.
471
   */
472
  function query() {
473

    
474
    if (!$this->is_batched()) {
475
      return parent::query();
476
    }
477

    
478
    // Make the query distinct if the option was set.
479
    if ($this->get_option('distinct')) {
480
      $this->view->query->set_distinct();
481
    }
482

    
483
    if (!empty($this->batched_execution_state->batch_state) && !empty($this->batched_execution_state->sandbox['weight_field_alias'])) {
484

    
485
      switch ($this->batched_execution_state->batch_state) {
486
        case VIEWS_DATA_EXPORT_BODY:
487
        case VIEWS_DATA_EXPORT_HEADER:
488
        case VIEWS_DATA_EXPORT_FOOTER:
489
          // Tell views its been executed.
490
          $this->view->executed = TRUE;
491

    
492
          // Grab our results from the index, and push them into the view result.
493
          // TODO: Handle external databases.
494
          $result = db_query_range('SELECT * FROM {' . $this->index_tablename() . '} ORDER BY ' . $this->batched_execution_state->sandbox['weight_field_alias'] . ' ASC', 0, $this->get_option('segment_size'));
495
          $this->view->result = array();
496
          foreach ($result as $item_hashed) {
497
            $item = new stdClass();
498
            // We had to shorten some of the column names in the index, restore
499
            // those now.
500
            foreach ($item_hashed as $hash => $value) {
501
              if (isset($this->batched_execution_state->sandbox['field_aliases'][$hash])) {
502
                $item->{$this->batched_execution_state->sandbox['field_aliases'][$hash]} = $value;
503
              }
504
              else {
505
                $item->{$hash} = $value;
506
              }
507
            }
508
            // Push the restored $item in the views result array.
509
            $this->view->result[] = $item;
510
          }
511
          $this->view->_post_execute();
512
          break;
513
      }
514
    }
515
  }
516

    
517

    
518
  /**
519
   * Render the 'Export Finished' page with the link to the file on it.
520
   */
521
  function render_complete() {
522
    $return_path = empty($_GET['return-url']) ? '' : $_GET['return-url'];
523

    
524
    $query = array(
525
      'download' => 1,
526
      'eid' => $this->batched_execution_state->eid,
527
    );
528

    
529
    return theme('views_data_export_complete_page', array(
530
      'file' => url($this->view->get_url(), array('query' => $query)),
531
      'errors' => $this->errors,
532
      'return_url' => $return_path));
533
  }
534

    
535
  /**
536
   * TBD - What does 'preview' mean for bulk exports?
537
   * According to doc:
538
   * "Fully render the display for the purposes of a live preview or
539
   * some other AJAXy reason. [views_plugin_display.inc:1877]"
540
   *
541
   * Not sure it makes sense for Bulk exports to be previewed in this manner?
542
   * We need the user's full attention to run the batch. Suggestions:
543
   * 1) Provide a link to execute the view?
544
   * 2) Provide a link to the last file we generated??
545
   * 3) Show a table of the first 20 results?
546
   */
547
  function preview() {
548
    if (!$this->is_batched()) {
549
      // Can replace with return parent::preview() when views 2.12 lands.
550
      if (!empty($this->view->live_preview)) {
551
        // Change the items per page.
552
        $this->view->set_items_per_page(20);
553
        // Force a pager to be used.
554
        $this->set_option('pager', array('type' => 'some', 'options' => array()));
555
        return '<p>' . t('A maximum of 20 items will be shown here, all results will be shown on export.') . '</p><pre>' . check_plain($this->view->render()) . '</pre>';
556
      }
557
      return $this->view->render();
558
    }
559
    return '';
560
  }
561

    
562
  /**
563
   * Transfer the output file to the client.
564
   */
565
  function transfer_file() {
566
    // Build the view so we can set the headers.
567
    $this->view->build();
568
    // Arguments can cause the style to not get built.
569
    if (!$this->view->init_style()) {
570
      $this->view->build_info['fail'] = TRUE;
571
    }
572
    // Set the headers.
573
    $this->add_http_headers();
574
    file_transfer($this->outputfile_path(), array());
575
  }
576

    
577
  /**
578
   * Called on export initialization.
579
   *
580
   * Modifies the view query to insert the results into a table, which we call
581
   * the 'index', this means we essentially have a snapshot of the results,
582
   * which we can then take time over rendering.
583
   *
584
   * This method is essentially all the best bits of the view::execute() method.
585
   */
586
  protected function initialize_index() {
587
    $view = &$this->view;
588
    // Get views to build the query.
589
    $view->build();
590

    
591
    // Change the query object to use our custom one.
592
    switch ($this->_get_database_driver()) {
593
      case 'pgsql':
594
        $query_class = 'views_data_export_plugin_query_pgsql_batched';
595
        break;
596

    
597
      default:
598
        $query_class = 'views_data_export_plugin_query_default_batched';
599
        break;
600
    }
601
    $query = new $query_class();
602
    // Copy the query over:
603
    foreach ($view->query as $property => $value) {
604
      $query->$property = $value;
605
    }
606
    // Replace the query object.
607
    $view->query = $query;
608

    
609
    $view->execute();
610
  }
611

    
612
  /**
613
   * Given a view, construct an map of hashed aliases to aliases.
614
   *
615
   * The keys of the returned array will have a maximum length of 33 characters.
616
   */
617
  function field_aliases_create(&$view) {
618
    $all_aliases = array();
619
    foreach ($view->query->fields as $field) {
620
      if (strlen($field['alias']) > 32) {
621
        $all_aliases['a' . md5($field['alias'])] = $field['alias'];
622
      }
623
      else {
624
        $all_aliases[$field['alias']] = $field['alias'];
625
      }
626
    }
627
    return $all_aliases;
628
  }
629

    
630
  /**
631
   * Create an alias for the weight field in the index.
632
   *
633
   * This method ensures that it isn't the same as any other alias in the
634
   * supplied view's fields.
635
   */
636
  function _weight_alias_create(&$view) {
637
    $alias = 'vde_weight';
638
    $all_aliases = array();
639
    foreach ($view->query->fields as $field) {
640
      $all_aliases[] = $field['alias'];
641
    }
642
    // Keep appending '_' until we are unique.
643
    while (in_array($alias, $all_aliases)) {
644
      $alias .= '_';
645
    }
646
    return $alias;
647
  }
648

    
649
  /**
650
   * Remove the index.
651
   */
652
  function remove_index() {
653
    $ret = array();
654
    if (db_table_exists($this->index_tablename())) {
655
      db_drop_table($this->index_tablename());
656
    }
657
  }
658

    
659
  /**
660
   * Return the name of the unique table to store the index in.
661
   */
662
  function index_tablename() {
663
    return VIEWS_DATA_EXPORT_INDEX_TABLE_PREFIX . $this->batched_execution_state->eid;
664
  }
665

    
666
  /**
667
   * Get the output file path.
668
   */
669
  function outputfile_path() {
670
    if (empty($this->_output_file)) {
671
      if (!empty($this->batched_execution_state->fid)) {
672
        // Return the filename associated with this file.
673
        $this->_output_file = $this->file_load($this->batched_execution_state->fid);
674
      }
675
      else {
676
        return NULL;
677
      }
678
    }
679
    return $this->_output_file->uri;
680
  }
681

    
682
  /**
683
   * Called on export initialization
684
   * Creates the output file, registers it as a temporary file with Drupal
685
   * and returns the fid
686
   */
687
  protected function outputfile_create() {
688

    
689
    $dir = variable_get('views_data_export_directory', 'temporary://views_plugin_display');
690

    
691
    // Make sure the directory exists first.
692
    if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
693
      $this->abort_export(t('Could not create temporary directory for result export (@dir). Check permissions.', array ('@dir' => $dir)));
694
    }
695

    
696
    $path = drupal_tempnam($dir, 'views_data_export');
697

    
698
    // Save the file into the DB.
699
    $file = $this->file_save_file($path);
700

    
701
    return $file->fid;
702
  }
703

    
704
  /**
705
   * Write to the output file.
706
   */
707
  protected function outputfile_write($string) {
708
    $output_file = $this->outputfile_path();
709
    if (file_put_contents($output_file, $string, FILE_APPEND) === FALSE) {
710
      $this->abort_export(t('Could not write to temporary output file for result export (@file). Check permissions.', array ('@file' => $output_file)));
711
    }
712
  }
713

    
714
  function abort_export($errors) {
715
    // Just cause the next batch to do the clean-up
716
    if (!is_array($errors)) {
717
      $errors = array($errors);
718
    }
719
    foreach ($errors as $error) {
720
      drupal_set_message($error . ' ['. t('Export Aborted') . ']', 'error');
721
    }
722
    $this->batched_execution_state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
723
  }
724

    
725
  /**
726
    * Load a file from the database.
727
    *
728
    * @param $fid
729
    *   A numeric file id or string containing the file path.
730
    * @return
731
    *   A file object.
732
    */
733
  function file_load($fid) {
734
    return file_load($fid);
735
  }
736

    
737
  /**
738
  * Save a file into a file node after running all the associated validators.
739
  *
740
  * This function is usually used to move a file from the temporary file
741
  * directory to a permanent location. It may be used by import scripts or other
742
  * modules that want to save an existing file into the database.
743
  *
744
  * @param $filepath
745
  *   The local file path of the file to be saved.
746
  * @return
747
  *   An array containing the file information, or 0 in the event of an error.
748
  */
749
  function file_save_file($filepath) {
750
    return file_save_data('', $filepath, FILE_EXISTS_REPLACE);
751
  }
752

    
753
  /**
754
   * Helper function that computes the time remaining
755
   */
756
  function compute_time_remaining($started, $finished) {
757
    list($usec, $sec) = explode(' ', microtime());
758
    $now = (float) $usec + (float) $sec;
759
    $diff = round(($now - $started), 0);
760
    // So we've taken $diff seconds to get this far.
761
    if ($finished > 0) {
762
      $estimate_total = $diff / $finished;
763
      $stamp = max(1, $estimate_total - $diff);
764
      // Round up to nearest 30 seconds.
765
      $stamp = ceil($stamp / 30) * 30;
766
      // Set the message in the batch context.
767
      return t('Time remaining: about @interval.', array('@interval' => format_interval($stamp)));
768
    }
769
  }
770

    
771
  /**
772
   * Checks the driver of the database underlying
773
   * this query and returns FALSE if it is imcompatible
774
   * with the approach taken in this display.
775
   * Basically mysql & mysqli will be fine, pg will not
776
   */
777
  function is_compatible() {
778
    $incompatible_drivers = array (
779
      //'pgsql',
780
    );
781
    $db_driver = $this->_get_database_driver();
782
    return !in_array($db_driver, $incompatible_drivers);
783
  }
784

    
785
  function  _get_database_driver() {
786
    $name = !empty($this->view->base_database) ? $this->view->base_database : 'default';
787
    $conn_info = Database::getConnectionInfo($name);
788
    return $conn_info['default']['driver'];
789
  }
790
}
791

    
792
class views_data_export_plugin_query_default_batched extends views_plugin_query_default {
793

    
794

    
795
  /**
796
   * Executes the query and fills the associated view object with according
797
   * values.
798
   *
799
   * Values to set: $view->result, $view->total_rows, $view->execute_time,
800
   * $view->current_page.
801
   */
802
  function execute(&$view) {
803
    $display_handler = &$view->display_handler;
804
    $external = FALSE; // Whether this query will run against an external database.
805
    $query = $view->build_info['query'];
806
    $count_query = $view->build_info['count_query'];
807

    
808
    $query->addMetaData('view', $view);
809
    $count_query->addMetaData('view', $view);
810

    
811
    if (empty($this->options['disable_sql_rewrite'])) {
812
      $base_table_data = views_fetch_data($this->base_table);
813
      if (isset($base_table_data['table']['base']['access query tag'])) {
814
        $access_tag = $base_table_data['table']['base']['access query tag'];
815
        $query->addTag($access_tag);
816
        $count_query->addTag($access_tag);
817
      }
818
    }
819

    
820
    $items = array();
821
    if ($query) {
822
      $additional_arguments = module_invoke_all('views_query_substitutions', $view);
823

    
824
      // Count queries must be run through the preExecute() method.
825
      // If not, then hook_query_node_access_alter() may munge the count by
826
      // adding a distinct against an empty query string
827
      // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
828
      // See pager.inc > PagerDefault::execute()
829
      // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
830
      // See http://drupal.org/node/1046170.
831
      $count_query->preExecute();
832

    
833
      // Build the count query.
834
      $count_query = $count_query->countQuery();
835

    
836
      // Add additional arguments as a fake condition.
837
      // XXX: this doesn't work... because PDO mandates that all bound arguments
838
      // are used on the query. TODO: Find a better way to do this.
839
      if (!empty($additional_arguments)) {
840
        // $query->where('1 = 1', $additional_arguments);
841
        // $count_query->where('1 = 1', $additional_arguments);
842
      }
843

    
844
      $start = microtime(TRUE);
845

    
846
      if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
847
        $this->pager->execute_count_query($count_query);
848
      }
849

    
850
      // Let the pager modify the query to add limits.
851
      $this->pager->pre_execute($query);
852

    
853
      if (!empty($this->limit) || !empty($this->offset)) {
854
        // We can't have an offset without a limit, so provide a very large limit instead.
855
        $limit  = intval(!empty($this->limit) ? $this->limit : 999999);
856
        $offset = intval(!empty($this->offset) ? $this->offset : 0);
857
        $query->range($offset, $limit);
858
      }
859

    
860
      try {
861
        // The $query is final and ready to go, we are going to redirect it to
862
        // become an insert into our table, sneaky!
863
        // Our query will look like:
864
        // CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
865
        // (-query-) AS cl, (SELECT @row := 0) AS r
866
        // We do some magic to get the row count.
867

    
868
        $display_handler->batched_execution_state->sandbox['weight_field_alias'] = $display_handler->_weight_alias_create($view);
869

    
870
        $display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
871
        $select_aliases = array();
872
        foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
873
          $select_aliases[] = "cl.$alias AS $hash";
874
        }
875

    
876
        // TODO: this could probably be replaced with a query extender and new query type.
877
        $query->preExecute();
878
        $args = $query->getArguments();
879
        $insert_query = 'CREATE TABLE {' . $display_handler->index_tablename() . '} SELECT @row := @row + 1 AS ' . $display_handler->batched_execution_state->sandbox['weight_field_alias'] . ', ' . implode(', ', $select_aliases) . ' FROM (' . (string)$query . ') AS cl, (SELECT @row := 0) AS r';
880
        db_query($insert_query, $args);
881

    
882

    
883
        $view->result = array();
884

    
885
        $this->pager->post_execute($view->result);
886

    
887
        if ($this->pager->use_pager()) {
888
          $view->total_rows = $this->pager->get_total_items();
889
        }
890

    
891
        // Now create an index for the weight field, otherwise the queries on the
892
        // index will take a long time to execute.
893
        db_add_unique_key($display_handler->index_tablename(), $display_handler->batched_execution_state->sandbox['weight_field_alias'], array($display_handler->batched_execution_state->sandbox['weight_field_alias']));
894
      }
895
      catch (Exception $e) {
896
        $view->result = array();
897
        debug('Exception: ' . $e->getMessage());
898
      }
899

    
900
    }
901
    $view->execute_time = microtime(TRUE) - $start;
902
  }
903
}
904

    
905
class views_data_export_plugin_query_pgsql_batched extends views_data_export_plugin_query_default_batched {
906

    
907

    
908
  /**
909
   * Executes the query and fills the associated view object with according
910
   * values.
911
   *
912
   * Values to set: $view->result, $view->total_rows, $view->execute_time,
913
   * $view->current_page.
914
   */
915
  function execute(&$view) {
916
    $display_handler = &$view->display_handler;
917
    $external = FALSE; // Whether this query will run against an external database.
918
    $query = $view->build_info['query'];
919
    $count_query = $view->build_info['count_query'];
920

    
921
    $query->addMetaData('view', $view);
922
    $count_query->addMetaData('view', $view);
923

    
924
    if (empty($this->options['disable_sql_rewrite'])) {
925
      $base_table_data = views_fetch_data($this->base_table);
926
      if (isset($base_table_data['table']['base']['access query tag'])) {
927
        $access_tag = $base_table_data['table']['base']['access query tag'];
928
        $query->addTag($access_tag);
929
        $count_query->addTag($access_tag);
930
      }
931
    }
932

    
933
    $items = array();
934
    if ($query) {
935
      $additional_arguments = module_invoke_all('views_query_substitutions', $view);
936

    
937
      // Count queries must be run through the preExecute() method.
938
      // If not, then hook_query_node_access_alter() may munge the count by
939
      // adding a distinct against an empty query string
940
      // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
941
      // See pager.inc > PagerDefault::execute()
942
      // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
943
      // See http://drupal.org/node/1046170.
944
      $count_query->preExecute();
945

    
946
      // Build the count query.
947
      $count_query = $count_query->countQuery();
948

    
949
      // Add additional arguments as a fake condition.
950
      // XXX: this doesn't work... because PDO mandates that all bound arguments
951
      // are used on the query. TODO: Find a better way to do this.
952
      if (!empty($additional_arguments)) {
953
        // $query->where('1 = 1', $additional_arguments);
954
        // $count_query->where('1 = 1', $additional_arguments);
955
      }
956

    
957
      $start = microtime(TRUE);
958

    
959
      if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
960
        $this->pager->execute_count_query($count_query);
961
      }
962

    
963
      // Let the pager modify the query to add limits.
964
      $this->pager->pre_execute($query);
965

    
966
      if (!empty($this->limit) || !empty($this->offset)) {
967
        // We can't have an offset without a limit, so provide a very large limit instead.
968
        $limit  = intval(!empty($this->limit) ? $this->limit : 999999);
969
        $offset = intval(!empty($this->offset) ? $this->offset : 0);
970
        $query->range($offset, $limit);
971
      }
972

    
973
      try {
974
        // The $query is final and ready to go, we are going to redirect it to
975
        // become an insert into our table, sneaky!
976
        // Our query will look like:
977
        // CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
978
        // (-query-) AS cl, (SELECT @row := 0) AS r
979
        // We do some magic to get the row count.
980

    
981
        $display_handler->batched_execution_state->sandbox['weight_field_alias'] = $display_handler->_weight_alias_create($view);
982

    
983
        $display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
984
        $select_aliases = array();
985
        foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
986
          $select_aliases[] = "cl.$alias AS $hash";
987
        }
988

    
989
        // TODO: this could probably be replaced with a query extender and new query type.
990
        $query->preExecute();
991
        $args = $query->getArguments();
992
        // Create temporary sequence
993
        $seq_name = $display_handler->index_tablename() . '_seq';
994
        db_query('CREATE TEMP sequence ' . $seq_name);
995
        // Query uses sequence to create row number
996
        $insert_query = 'CREATE TABLE {' . $display_handler->index_tablename() . "} AS SELECT nextval('". $seq_name . "') AS " . $display_handler->batched_execution_state->sandbox['weight_field_alias'] . ', ' . implode(', ', $select_aliases) . ' FROM (' . (string)$query . ') AS cl';
997
        db_query($insert_query, $args);
998

    
999

    
1000
        $view->result = array();
1001

    
1002
        $this->pager->post_execute($view->result);
1003

    
1004
        if ($this->pager->use_pager()) {
1005
          $view->total_rows = $this->pager->get_total_items();
1006
        }
1007

    
1008
        // Now create an index for the weight field, otherwise the queries on the
1009
        // index will take a long time to execute.
1010
        db_add_unique_key($display_handler->index_tablename(), $display_handler->batched_execution_state->sandbox['weight_field_alias'], array($display_handler->batched_execution_state->sandbox['weight_field_alias']));
1011
      }
1012
      catch (Exception $e) {
1013
        $view->result = array();
1014
        debug('Exception: ' . $e->getMessage());
1015
      }
1016

    
1017
    }
1018
    $view->execute_time = microtime(TRUE) - $start;
1019
  }
1020
}