Project

General

Profile

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

root / drupal7 / sites / all / modules / views_data_export / plugins / views_data_export_plugin_display_export.inc @ 56aebcb7

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

    
242
    if (!empty($_GET['eid']) && !empty($_GET['token']) && drupal_valid_token($_GET['token'], 'views_data_export/' . $_GET['eid'])) {
243
      $eid = $_GET['eid'];
244
    }
245
    elseif (!empty($this->batched_execution_state->eid)) {
246
      $eid = $this->batched_execution_state->eid;
247
    }
248
    else {
249
      $eid = FALSE;
250
    }
251

    
252
    if ($eid) {
253
      $this->batched_execution_state = views_data_export_get($eid);
254
    }
255

    
256
    // First time through
257
    if (empty($this->batched_execution_state)) {
258
      $output = $this->execute_initial();
259
    }
260

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

    
277
    // Last time through
278
    if ($this->batched_execution_state->batch_state == VIEWS_DATA_EXPORT_FINISHED) {
279
      $output = $this->execute_final();
280
    }
281
    // In the middle of processing
282
    else {
283
      $output = $this->execute_normal();
284
    }
285

    
286
    //Ensure any changes we made to the database sandbox are saved
287
    views_data_export_update($this->batched_execution_state);
288

    
289
    return $output;
290
  }
291

    
292

    
293
  /**
294
   * Initializes the whole export process and starts off the batch process.
295
   *
296
   * Page execution will be ended at the end of this function.
297
   */
298
  function execute_initial() {
299

    
300
    // Register this export with our central table - get a unique eid
301
    // Also store our view in a cache to be retrieved with each batch call
302
    $this->batched_execution_state = views_data_export_new($this->view->name, $this->view->current_display, $this->outputfile_create());
303
    views_data_export_view_store($this->batched_execution_state->eid, $this->view);
304

    
305
    // Record a usage of our file, so we can identify our exports later.
306
    file_usage_add(file_load($this->batched_execution_state->fid), 'views_data_export', 'eid', $this->batched_execution_state->eid);
307

    
308
    // We need to build the index right now, before we lose $_GET etc.
309
    $this->initialize_index();
310
    //$this->batched_execution_state->fid = $this->outputfile_create();
311

    
312
    // Initialize the progress counter.
313
    if (db_table_exists($this->index_tablename())) {
314
      $this->batched_execution_state->sandbox['max'] = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
315
    }
316

    
317
    // Record the time we started.
318
    list($usec, $sec) = explode(' ', microtime());
319
    $this->batched_execution_state->sandbox['started'] = (float) $usec + (float) $sec;
320

    
321
    // Pop something into the session to ensure it stays aorund.
322
    $_SESSION['views_data_export'][$this->batched_execution_state->eid] = TRUE;
323

    
324
    // Build up our querystring for the final page callback.
325
    $querystring = array(
326
      'eid' => $this->batched_execution_state->eid,
327
      'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
328
      'return-url' => NULL,
329
    );
330

    
331
    // If we have a configured return path, use that.
332
    if ($this->get_option('return_path')) {
333
      $querystring['return-url'] = $this->get_option('return_path');
334
    }
335
    // Else if we were attached to another view, grab that for the final URL.
336
    else if (!empty($_GET['attach']) && isset($this->view->display[$_GET['attach']])) {
337
      // Get the path of the attached display:
338
      $querystring['return-url'] = $this->view->get_url(NULL, $this->view->display[$_GET['attach']]->handler->get_path());
339
    }
340

    
341
    //Set the batch off
342
    $batch = array(
343
      'operations' => array (
344
        array('_views_data_export_batch_process', array($this->batched_execution_state->eid, $this->view->current_display,  $this->view->get_exposed_input())),
345
      ),
346
      'title' => t('Building export'),
347
      'init_message' => t('Export is starting up.'),
348
      'progress_message' => t('Exporting @percentage% complete,'),
349
      'error_message' => t('Export has encountered an error.'),
350
    );
351

    
352
    // We do not return, so update database sandbox now
353
    views_data_export_update($this->batched_execution_state);
354

    
355
    $final_destination = $this->view->get_url();
356

    
357
    // Provide a way in for others at this point
358
    // e.g. Drush to grab this batch and yet execute it in
359
    // it's own special way
360
    $batch['view_name'] = $this->view->name;
361
    $batch['exposed_filters'] = $this->view->get_exposed_input();
362
    $batch['display_id'] = $this->view->current_display;
363
    $batch['eid'] = $this->batched_execution_state->eid;
364
    $batch_redirect = array($final_destination, array('query' => $querystring));
365
    drupal_alter('views_data_export_batch', $batch, $batch_redirect);
366

    
367
    // Modules may have cleared out $batch, indicating that we shouldn't process further.
368
    if (!empty($batch)) {
369
      batch_set($batch);
370
      // batch_process exits
371
      batch_process($batch_redirect);
372
    }
373
  }
374

    
375

    
376
  /**
377
   * Compiles the next chunk of the output file
378
   */
379
  function execute_normal() {
380

    
381
    // Pass through to our render method,
382
    $output = $this->view->render();
383

    
384
    // Append what was rendered to the output file.
385
    $this->outputfile_write($output);
386

    
387
    // Store for convenience.
388
    $state = &$this->batched_execution_state;
389
    $sandbox = &$state->sandbox;
390

    
391
    // Update progress measurements & move our state forward
392
    switch ($state->batch_state) {
393

    
394
      case VIEWS_DATA_EXPORT_BODY:
395
        // Remove rendered results from our index
396
        if (count($this->view->result) && ($sandbox['weight_field_alias'])) {
397
          $last = end($this->view->result);
398
          db_delete($this->index_tablename())
399
            ->condition($sandbox['weight_field_alias'], $last->{$sandbox['weight_field_alias']}, '<=')
400
            ->execute();
401

    
402
          // Update progress.
403
          $progress = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
404
          // TODO: These next few lines are messy, clean them up.
405
          $progress = 0.99 - ($progress / $sandbox['max'] * 0.99);
406
          $progress = ((int)floor($progress * 100000));
407
          $progress = $progress / 100000;
408
          $sandbox['finished'] = $progress;
409
        }
410
        else {
411
          // No more results.
412
          $progress = 0.99;
413
          $state->batch_state = VIEWS_DATA_EXPORT_FOOTER;
414
        }
415
        break;
416

    
417
      case VIEWS_DATA_EXPORT_HEADER:
418
        $sandbox['finished'] = 0;
419
        $state->batch_state = VIEWS_DATA_EXPORT_BODY;
420
        break;
421

    
422
      case VIEWS_DATA_EXPORT_FOOTER:
423
        // Update the temporary file size, otherwise we would get a problematic
424
        // "Content-Length: 0" HTTP header, that may break the export download.
425
        $this->outputfile_update_size();
426
        $sandbox['finished'] = 1;
427
        $state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
428
        break;
429
    }
430

    
431
    // Create a more helpful exporting message.
432
    $sandbox['message'] = $this->compute_time_remaining($sandbox['started'], $sandbox['finished']);
433
  }
434

    
435

    
436
  /**
437
   * Renders the final page
438
   *  We should be free of the batch at this point
439
   */
440
  function execute_final() {
441
    // Should we download the file.
442
    if (!empty($_GET['download'])) {
443
      // Clean up our session, if we need to.
444
      if (isset($_SESSION)) {
445
        unset($_SESSION['views_data_export'][$this->batched_execution_state->eid]);
446
        if (empty($_SESSION['views_data_export'])) {
447
          unset($_SESSION['views_data_export']);
448
        }
449
      }
450
      // This next method will exit.
451
      $this->transfer_file();
452
    }
453
    else {
454
      // Remove the index table.
455
      $this->remove_index();
456
      return $this->render_complete();
457
    }
458
  }
459

    
460

    
461
  /**
462
   * Render the display.
463
   *
464
   * We basically just work out if we should be rendering the header, body or
465
   * footer and call the appropriate functions on the style plugins.
466
   */
467
  function render() {
468

    
469
    if (!$this->is_batched()) {
470
      $result = parent::render();
471
      if (empty($this->view->live_preview)) {
472
        $this->add_http_headers();
473
      }
474
      return $result;
475
    }
476

    
477
    $this->view->build();
478

    
479
    switch ($this->batched_execution_state->batch_state) {
480
      case VIEWS_DATA_EXPORT_BODY:
481
        $output = $this->view->style_plugin->render_body();
482
        break;
483
      case VIEWS_DATA_EXPORT_HEADER:
484
        $output = $this->view->style_plugin->render_header();
485
        break;
486
      case VIEWS_DATA_EXPORT_FOOTER:
487
        $output = $this->view->style_plugin->render_footer();
488
        break;
489
    }
490

    
491
    return $output;
492
  }
493

    
494

    
495

    
496
  /**
497
   * Trick views into thinking that we have executed the query and got results.
498
   *
499
   * We are called in the build phase of the view, but short circuit straight to
500
   * getting the results and making the view think it has already executed the
501
   * query.
502
   */
503
  function query() {
504

    
505
    if (!$this->is_batched()) {
506
      return parent::query();
507
    }
508

    
509
    // Make the query distinct if the option was set.
510
    if ($this->get_option('distinct')) {
511
      $this->view->query->set_distinct();
512
    }
513

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

    
516
      switch ($this->batched_execution_state->batch_state) {
517
        case VIEWS_DATA_EXPORT_BODY:
518
        case VIEWS_DATA_EXPORT_HEADER:
519
        case VIEWS_DATA_EXPORT_FOOTER:
520
          // Tell views its been executed.
521
          $this->view->executed = TRUE;
522

    
523
          // Grab our results from the index, and push them into the view result.
524
          // TODO: Handle external databases.
525
          $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'));
526
          $this->view->result = array();
527
          foreach ($result as $item_hashed) {
528
            $item = new stdClass();
529
            // We had to shorten some of the column names in the index, restore
530
            // those now.
531
            foreach ($item_hashed as $hash => $value) {
532
              if (isset($this->batched_execution_state->sandbox['field_aliases'][$hash])) {
533
                $item->{$this->batched_execution_state->sandbox['field_aliases'][$hash]} = $value;
534
              }
535
              else {
536
                $item->{$hash} = $value;
537
              }
538
            }
539
            // Push the restored $item in the views result array.
540
            $this->view->result[] = $item;
541
          }
542
          $this->view->_post_execute();
543
          break;
544
      }
545
    }
546
  }
547

    
548

    
549
  /**
550
   * Render the 'Export Finished' page with the link to the file on it.
551
   */
552
  function render_complete() {
553
    $return_path = empty($_GET['return-url']) ? '' : $_GET['return-url'];
554

    
555
    $query = array(
556
      'download' => 1,
557
      'eid' => $this->batched_execution_state->eid,
558
      'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
559
    );
560

    
561
    return theme('views_data_export_complete_page', array(
562
      'file' => url($this->view->get_url(), array('query' => $query)),
563
      'errors' => $this->errors,
564
      'return_url' => $return_path));
565
  }
566

    
567
  /**
568
   * TBD - What does 'preview' mean for bulk exports?
569
   * According to doc:
570
   * "Fully render the display for the purposes of a live preview or
571
   * some other AJAXy reason. [views_plugin_display.inc:1877]"
572
   *
573
   * Not sure it makes sense for Bulk exports to be previewed in this manner?
574
   * We need the user's full attention to run the batch. Suggestions:
575
   * 1) Provide a link to execute the view?
576
   * 2) Provide a link to the last file we generated??
577
   * 3) Show a table of the first 20 results?
578
   */
579
  function preview() {
580
    if (!$this->is_batched()) {
581
      // Can replace with return parent::preview() when views 2.12 lands.
582
      if (!empty($this->view->live_preview)) {
583
        // Change the items per page.
584
        $this->view->set_items_per_page(20);
585
        // Force a pager to be used.
586
        $this->set_option('pager', array('type' => 'some', 'options' => array()));
587
        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>';
588
      }
589
      return $this->view->render();
590
    }
591
    return '';
592
  }
593

    
594
  /**
595
   * Transfer the output file to the client.
596
   */
597
  function transfer_file() {
598
    // Build the view so we can set the headers.
599
    $this->view->build();
600
    // Arguments can cause the style to not get built.
601
    if (!$this->view->init_style()) {
602
      $this->view->build_info['fail'] = TRUE;
603
    }
604
    // Set the headers.
605
    $this->add_http_headers();
606
    $headers = array(
607
      'Content-Length' => $this->outputfile_entity()->filesize,
608
    );
609
    file_transfer($this->outputfile_path(), $headers);
610
  }
611

    
612
  /**
613
   * Called on export initialization.
614
   *
615
   * Modifies the view query to insert the results into a table, which we call
616
   * the 'index', this means we essentially have a snapshot of the results,
617
   * which we can then take time over rendering.
618
   *
619
   * This method is essentially all the best bits of the view::execute() method.
620
   */
621
  protected function initialize_index() {
622
    $view = &$this->view;
623
    // Get views to build the query.
624
    $view->build();
625

    
626
    // Change the query object to use our custom one.
627
    switch ($this->_get_database_driver()) {
628
      case 'pgsql':
629
        $query_class = 'views_data_export_plugin_query_pgsql_batched';
630
        break;
631

    
632
      default:
633
        $query_class = 'views_data_export_plugin_query_default_batched';
634
        break;
635
    }
636
    $query = new $query_class();
637
    // Copy the query over:
638
    foreach ($view->query as $property => $value) {
639
      $query->$property = $value;
640
    }
641
    // Replace the query object.
642
    $view->query = $query;
643

    
644
    $view->execute();
645
  }
646

    
647
  /**
648
   * Given a view, construct an map of hashed aliases to aliases.
649
   *
650
   * The keys of the returned array will have a maximum length of 33 characters.
651
   */
652
  function field_aliases_create(&$view) {
653
    $all_aliases = array();
654
    foreach ($view->query->fields as $field) {
655
      if (strlen($field['alias']) > 32) {
656
        $all_aliases['a' . md5($field['alias'])] = $field['alias'];
657
      }
658
      else {
659
        $all_aliases[$field['alias']] = $field['alias'];
660
      }
661
    }
662
    return $all_aliases;
663
  }
664

    
665
  /**
666
   * Create an alias for the weight field in the index.
667
   *
668
   * This method ensures that it isn't the same as any other alias in the
669
   * supplied view's fields.
670
   */
671
  function _weight_alias_create(&$view) {
672
    $alias = 'vde_weight';
673
    $all_aliases = array();
674
    foreach ($view->query->fields as $field) {
675
      $all_aliases[] = $field['alias'];
676
    }
677
    // Keep appending '_' until we are unique.
678
    while (in_array($alias, $all_aliases)) {
679
      $alias .= '_';
680
    }
681
    return $alias;
682
  }
683

    
684
  /**
685
   * Remove the index.
686
   */
687
  function remove_index() {
688
    $ret = array();
689
    if (db_table_exists($this->index_tablename())) {
690
      db_drop_table($this->index_tablename());
691
    }
692
  }
693

    
694
  /**
695
   * Return the name of the unique table to store the index in.
696
   */
697
  function index_tablename() {
698
    return VIEWS_DATA_EXPORT_INDEX_TABLE_PREFIX . $this->batched_execution_state->eid;
699
  }
700

    
701
  /**
702
   * Get the output file entity.
703
   */
704
  public function outputfile_entity() {
705
    if (empty($this->_output_file)) {
706
      if (!empty($this->batched_execution_state->fid)) {
707
        // Return the filename associated with this file.
708
        $this->_output_file = $this->file_load($this->batched_execution_state->fid);
709
      }
710
      else {
711
        return NULL;
712
      }
713
    }
714
    return $this->_output_file;
715
  }
716

    
717
  /**
718
   * Get the output file path.
719
   */
720
  public function outputfile_path() {
721
    if ($file = $this->outputfile_entity()) {
722
      return $file->uri;
723
    }
724
  }
725

    
726
  /**
727
   * Called on export initialization
728
   * Creates the output file, registers it as a temporary file with Drupal
729
   * and returns the fid
730
   */
731
  protected function outputfile_create() {
732

    
733
    $dir = variable_get('views_data_export_directory', 'temporary://views_plugin_display');
734

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

    
740
    $path = drupal_tempnam($dir, 'views_data_export');
741

    
742
    // Save the file into the DB.
743
    $file = $this->file_save_file($path);
744

    
745
    // Make sure the file is marked as temporary.
746
    // There is no FILE_STATUS_TEMPORARY constant.
747
    $file->status = 0;
748
    file_save($file);
749

    
750
    return $file->fid;
751
  }
752

    
753
  /**
754
   * Write to the output file.
755
   */
756
  protected function outputfile_write($string) {
757
    $output_file = $this->outputfile_path();
758
    if (file_put_contents($output_file, $string, FILE_APPEND) === FALSE) {
759
      $this->abort_export(t('Could not write to temporary output file for result export (@file). Check permissions.', array ('@file' => $output_file)));
760
    }
761
  }
762

    
763
  /**
764
   * Updates the file size in the file entity.
765
   */
766
  protected function outputfile_update_size() {
767
    if ($file = $this->outputfile_entity()) {
768
      $file->filesize = filesize($file->uri);
769
      file_save($file);
770
    }
771
  }
772

    
773
  function abort_export($errors) {
774
    // Just cause the next batch to do the clean-up
775
    if (!is_array($errors)) {
776
      $errors = array($errors);
777
    }
778
    foreach ($errors as $error) {
779
      drupal_set_message($error . ' ['. t('Export Aborted') . ']', 'error');
780
    }
781
    $this->batched_execution_state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
782
  }
783

    
784
  /**
785
    * Load a file from the database.
786
    *
787
    * @param $fid
788
    *   A numeric file id or string containing the file path.
789
    * @return
790
    *   A file object.
791
    */
792
  function file_load($fid) {
793
    return file_load($fid);
794
  }
795

    
796
  /**
797
  * Save a file into a file node after running all the associated validators.
798
  *
799
  * This function is usually used to move a file from the temporary file
800
  * directory to a permanent location. It may be used by import scripts or other
801
  * modules that want to save an existing file into the database.
802
  *
803
  * @param $filepath
804
  *   The local file path of the file to be saved.
805
  * @return
806
  *   An array containing the file information, or 0 in the event of an error.
807
  */
808
  function file_save_file($filepath) {
809
    return file_save_data('', $filepath, FILE_EXISTS_REPLACE);
810
  }
811

    
812
  /**
813
   * Helper function that computes the time remaining
814
   */
815
  function compute_time_remaining($started, $finished) {
816
    list($usec, $sec) = explode(' ', microtime());
817
    $now = (float) $usec + (float) $sec;
818
    $diff = round(($now - $started), 0);
819
    // So we've taken $diff seconds to get this far.
820
    if ($finished > 0) {
821
      $estimate_total = $diff / $finished;
822
      $stamp = max(1, $estimate_total - $diff);
823
      // Round up to nearest 30 seconds.
824
      $stamp = ceil($stamp / 30) * 30;
825
      // Set the message in the batch context.
826
      return t('Time remaining: about @interval.', array('@interval' => format_interval($stamp)));
827
    }
828
  }
829

    
830
  /**
831
   * Checks the driver of the database underlying
832
   * this query and returns FALSE if it is imcompatible
833
   * with the approach taken in this display.
834
   * Basically mysql & mysqli will be fine, pg will not
835
   */
836
  function is_compatible() {
837
    $incompatible_drivers = array (
838
      //'pgsql',
839
    );
840
    $db_driver = $this->_get_database_driver();
841
    return !in_array($db_driver, $incompatible_drivers);
842
  }
843

    
844
  function  _get_database_driver() {
845
    $name = !empty($this->view->base_database) ? $this->view->base_database : 'default';
846
    $conn_info = Database::getConnectionInfo($name);
847
    return $conn_info['default']['driver'];
848
  }
849
}
850

    
851
class views_data_export_plugin_query_default_batched extends views_plugin_query_default {
852

    
853

    
854
  /**
855
   * Executes the query and fills the associated view object with according
856
   * values.
857
   *
858
   * Values to set: $view->result, $view->total_rows, $view->execute_time,
859
   * $view->current_page.
860
   */
861
  function execute(&$view) {
862
    $display_handler = &$view->display_handler;
863
    $external = FALSE; // Whether this query will run against an external database.
864
    $query = $view->build_info['query'];
865
    $count_query = $view->build_info['count_query'];
866

    
867
    $query->addMetaData('view', $view);
868
    $count_query->addMetaData('view', $view);
869

    
870
    if (empty($this->options['disable_sql_rewrite'])) {
871
      $base_table_data = views_fetch_data($this->base_table);
872
      if (isset($base_table_data['table']['base']['access query tag'])) {
873
        $access_tag = $base_table_data['table']['base']['access query tag'];
874
        $query->addTag($access_tag);
875
        $count_query->addTag($access_tag);
876
      }
877
    }
878

    
879
    $items = array();
880
    if ($query) {
881
      $additional_arguments = module_invoke_all('views_query_substitutions', $view);
882

    
883
      // Count queries must be run through the preExecute() method.
884
      // If not, then hook_query_node_access_alter() may munge the count by
885
      // adding a distinct against an empty query string
886
      // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
887
      // See pager.inc > PagerDefault::execute()
888
      // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
889
      // See http://drupal.org/node/1046170.
890
      $count_query->preExecute();
891

    
892
      // Build the count query.
893
      $count_query = $count_query->countQuery();
894

    
895
      // Add additional arguments as a fake condition.
896
      // XXX: this doesn't work... because PDO mandates that all bound arguments
897
      // are used on the query. TODO: Find a better way to do this.
898
      if (!empty($additional_arguments)) {
899
        // $query->where('1 = 1', $additional_arguments);
900
        // $count_query->where('1 = 1', $additional_arguments);
901
      }
902

    
903
      $start = microtime(TRUE);
904

    
905
      if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
906
        $this->pager->execute_count_query($count_query);
907
      }
908

    
909
      // Let the pager modify the query to add limits.
910
      $this->pager->pre_execute($query);
911

    
912
      if (!empty($this->limit) || !empty($this->offset)) {
913
        // We can't have an offset without a limit, so provide a very large limit instead.
914
        $limit  = intval(!empty($this->limit) ? $this->limit : 999999);
915
        $offset = intval(!empty($this->offset) ? $this->offset : 0);
916
        $query->range($offset, $limit);
917
      }
918

    
919
      try {
920
        // The $query is final and ready to go, we are going to redirect it to
921
        // become an insert into our table, sneaky!
922
        // Our query will look like:
923
        // CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
924
        // (-query-) AS cl, (SELECT @row := 0) AS r
925
        // We do some magic to get the row count.
926

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

    
929
        $display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
930
        $select_aliases = array();
931
        foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
932
          $select_aliases[] = "cl.$alias AS $hash";
933
        }
934

    
935
        // TODO: this could probably be replaced with a query extender and new query type.
936
        $query->preExecute();
937
        $args = $query->getArguments();
938
        $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';
939
        db_query($insert_query, $args);
940

    
941

    
942
        $view->result = array();
943

    
944
        $this->pager->post_execute($view->result);
945

    
946
        if ($this->pager->use_pager()) {
947
          $view->total_rows = $this->pager->get_total_items();
948
        }
949

    
950
        // Now create an index for the weight field, otherwise the queries on the
951
        // index will take a long time to execute.
952
        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']));
953
      }
954
      catch (Exception $e) {
955
        $view->result = array();
956
        debug('Exception: ' . $e->getMessage());
957
      }
958

    
959
    }
960
    $view->execute_time = microtime(TRUE) - $start;
961
  }
962
}
963

    
964
class views_data_export_plugin_query_pgsql_batched extends views_data_export_plugin_query_default_batched {
965

    
966

    
967
  /**
968
   * Executes the query and fills the associated view object with according
969
   * values.
970
   *
971
   * Values to set: $view->result, $view->total_rows, $view->execute_time,
972
   * $view->current_page.
973
   */
974
  function execute(&$view) {
975
    $display_handler = &$view->display_handler;
976
    $external = FALSE; // Whether this query will run against an external database.
977
    $query = $view->build_info['query'];
978
    $count_query = $view->build_info['count_query'];
979

    
980
    $query->addMetaData('view', $view);
981
    $count_query->addMetaData('view', $view);
982

    
983
    if (empty($this->options['disable_sql_rewrite'])) {
984
      $base_table_data = views_fetch_data($this->base_table);
985
      if (isset($base_table_data['table']['base']['access query tag'])) {
986
        $access_tag = $base_table_data['table']['base']['access query tag'];
987
        $query->addTag($access_tag);
988
        $count_query->addTag($access_tag);
989
      }
990
    }
991

    
992
    $items = array();
993
    if ($query) {
994
      $additional_arguments = module_invoke_all('views_query_substitutions', $view);
995

    
996
      // Count queries must be run through the preExecute() method.
997
      // If not, then hook_query_node_access_alter() may munge the count by
998
      // adding a distinct against an empty query string
999
      // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
1000
      // See pager.inc > PagerDefault::execute()
1001
      // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
1002
      // See http://drupal.org/node/1046170.
1003
      $count_query->preExecute();
1004

    
1005
      // Build the count query.
1006
      $count_query = $count_query->countQuery();
1007

    
1008
      // Add additional arguments as a fake condition.
1009
      // XXX: this doesn't work... because PDO mandates that all bound arguments
1010
      // are used on the query. TODO: Find a better way to do this.
1011
      if (!empty($additional_arguments)) {
1012
        // $query->where('1 = 1', $additional_arguments);
1013
        // $count_query->where('1 = 1', $additional_arguments);
1014
      }
1015

    
1016
      $start = microtime(TRUE);
1017

    
1018
      if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
1019
        $this->pager->execute_count_query($count_query);
1020
      }
1021

    
1022
      // Let the pager modify the query to add limits.
1023
      $this->pager->pre_execute($query);
1024

    
1025
      if (!empty($this->limit) || !empty($this->offset)) {
1026
        // We can't have an offset without a limit, so provide a very large limit instead.
1027
        $limit  = intval(!empty($this->limit) ? $this->limit : 999999);
1028
        $offset = intval(!empty($this->offset) ? $this->offset : 0);
1029
        $query->range($offset, $limit);
1030
      }
1031

    
1032
      try {
1033
        // The $query is final and ready to go, we are going to redirect it to
1034
        // become an insert into our table, sneaky!
1035
        // Our query will look like:
1036
        // CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
1037
        // (-query-) AS cl, (SELECT @row := 0) AS r
1038
        // We do some magic to get the row count.
1039

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

    
1042
        $display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
1043
        $select_aliases = array();
1044
        foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
1045
          $select_aliases[] = "cl.$alias AS $hash";
1046
        }
1047

    
1048
        // TODO: this could probably be replaced with a query extender and new query type.
1049
        $query->preExecute();
1050
        $args = $query->getArguments();
1051
        // Create temporary sequence
1052
        $seq_name = $display_handler->index_tablename() . '_seq';
1053
        db_query('CREATE TEMP sequence ' . $seq_name);
1054
        // Query uses sequence to create row number
1055
        $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';
1056
        db_query($insert_query, $args);
1057

    
1058

    
1059
        $view->result = array();
1060

    
1061
        $this->pager->post_execute($view->result);
1062

    
1063
        if ($this->pager->use_pager()) {
1064
          $view->total_rows = $this->pager->get_total_items();
1065
        }
1066

    
1067
        // Now create an index for the weight field, otherwise the queries on the
1068
        // index will take a long time to execute.
1069
        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']));
1070
      }
1071
      catch (Exception $e) {
1072
        $view->result = array();
1073
        debug('Exception: ' . $e->getMessage());
1074
      }
1075

    
1076
    }
1077
    $view->execute_time = microtime(TRUE) - $start;
1078
  }
1079
}