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 @ f0456308

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
      'return-url' => NULL,
310
    );
311

    
312
    // If we have a configured return path, use that.
313
    if ($this->get_option('return_path')) {
314
      $querystring['return-url'] = $this->get_option('return_path');
315
    }
316
    // Else if we were attached to another view, grab that for the final URL.
317
    else if (!empty($_GET['attach']) && isset($this->view->display[$_GET['attach']])) {
318
      // Get the path of the attached display:
319
      $querystring['return-url'] = $this->view->get_url(NULL, $this->view->display[$_GET['attach']]->handler->get_path());
320
    }
321

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

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

    
336
    $final_destination = $this->view->get_url();
337

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

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

    
356

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

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

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

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

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

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

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

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

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

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

    
413

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

    
431

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

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

    
448
    $this->view->build();
449

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

    
462
    return $output;
463
  }
464

    
465

    
466

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

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

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

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

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

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

    
519

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

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

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

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

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

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

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

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

    
611
    $view->execute();
612
  }
613

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

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

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

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

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

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

    
691
    $dir = variable_get('views_data_export_directory', 'temporary://views_plugin_display');
692

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

    
698
    $path = drupal_tempnam($dir, 'views_data_export');
699

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

    
703
    return $file->fid;
704
  }
705

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

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

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

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

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

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

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

    
794
class views_data_export_plugin_query_default_batched extends views_plugin_query_default {
795

    
796

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

    
810
    $query->addMetaData('view', $view);
811
    $count_query->addMetaData('view', $view);
812

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

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

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

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

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

    
846
      $start = microtime(TRUE);
847

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

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

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

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

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

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

    
878
        // TODO: this could probably be replaced with a query extender and new query type.
879
        $query->preExecute();
880
        $args = $query->getArguments();
881
        $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';
882
        db_query($insert_query, $args);
883

    
884

    
885
        $view->result = array();
886

    
887
        $this->pager->post_execute($view->result);
888

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

    
893
        // Now create an index for the weight field, otherwise the queries on the
894
        // index will take a long time to execute.
895
        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']));
896
      }
897
      catch (Exception $e) {
898
        $view->result = array();
899
        debug('Exception: ' . $e->getMessage());
900
      }
901

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

    
907
class views_data_export_plugin_query_pgsql_batched extends views_data_export_plugin_query_default_batched {
908

    
909

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

    
923
    $query->addMetaData('view', $view);
924
    $count_query->addMetaData('view', $view);
925

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

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

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

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

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

    
959
      $start = microtime(TRUE);
960

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

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

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

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

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

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

    
991
        // TODO: this could probably be replaced with a query extender and new query type.
992
        $query->preExecute();
993
        $args = $query->getArguments();
994
        // Create temporary sequence
995
        $seq_name = $display_handler->index_tablename() . '_seq';
996
        db_query('CREATE TEMP sequence ' . $seq_name);
997
        // Query uses sequence to create row number
998
        $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';
999
        db_query($insert_query, $args);
1000

    
1001

    
1002
        $view->result = array();
1003

    
1004
        $this->pager->post_execute($view->result);
1005

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

    
1010
        // Now create an index for the weight field, otherwise the queries on the
1011
        // index will take a long time to execute.
1012
        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']));
1013
      }
1014
      catch (Exception $e) {
1015
        $view->result = array();
1016
        debug('Exception: ' . $e->getMessage());
1017
      }
1018

    
1019
    }
1020
    $view->execute_time = microtime(TRUE) - $start;
1021
  }
1022
}