Project

General

Profile

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

root / drupal7 / sites / all / modules / webform / includes / webform.report.inc @ 7b2d1845

1
<?php
2

    
3
/**
4
 * @file
5
 * This file includes helper functions for creating reports for webform.module.
6
 *
7
 * @author Nathan Haug <nate@lullabot.com>
8
 */
9

    
10
// All functions within this file need the webform.submissions.inc.
11
module_load_include('inc', 'webform', 'includes/webform.submissions');
12

    
13
/**
14
 * Retrieve lists of submissions for a given webform.
15
 */
16
function webform_results_submissions($node, $user_filter, $pager_count) {
17
  global $user;
18

    
19
  // Determine whether views or hard-coded tables should be used for the
20
  // submissions table.
21
  if (!webform_variable_get('webform_table')) {
22
    // Load the submissions view.
23
    $view = webform_get_view($node, 'webform_submissions');
24
    if ($user_filter) {
25
      if ($user->uid) {
26
        drupal_set_title(t('Submissions for %user', array('%user' => $user->name)), PASS_THROUGH);
27
      }
28
      else {
29
        drupal_set_title(t('Your submissions'));
30
        webform_disable_page_cache();
31
      }
32
      return $view->preview('default', array($node->nid, $user->uid));
33
    }
34
    else {
35
      return $view->preview('default', array($node->nid));
36
    }
37
  }
38

    
39
  if (isset($_GET['results']) && is_numeric($_GET['results'])) {
40
    $pager_count = $_GET['results'];
41
  }
42

    
43
  $header = theme('webform_results_submissions_header', array('node' => $node));
44
  if ($user_filter) {
45
    if ($user->uid) {
46
      drupal_set_title(t('Submissions for %user', array('%user' => $user->name)), PASS_THROUGH);
47
    }
48
    else {
49
      drupal_set_title(t('Your submissions'));
50
      webform_disable_page_cache();
51
    }
52
    $submissions = webform_get_submissions(array('nid' => $node->nid, 'uid' => $user->uid), $header, $pager_count);
53
    $count = webform_get_submission_count($node->nid, $user->uid, NULL);
54
  }
55
  else {
56
    $submissions = webform_get_submissions($node->nid, $header, $pager_count);
57
    $count = webform_get_submission_count($node->nid, NULL, NULL);
58
  }
59

    
60
  $operation_column = end($header);
61
  $operation_total = $operation_column['colspan'];
62

    
63
  $rows = array();
64
  foreach ($submissions as $sid => $submission) {
65
    $row = array(
66
      $submission->is_draft ? t('@serial (draft)', array('@serial' => $submission->serial)) : $submission->serial,
67
      format_date($submission->submitted, 'short'),
68
    );
69
    if (webform_results_access($node, $user)) {
70
      $row[] = theme('username', array('account' => $submission));
71
      $row[] = $submission->remote_addr;
72
    }
73
    $row[] = l(t('View'), "node/$node->nid/submission/$sid");
74
    $operation_count = 1;
75
    // No need to call this multiple times, just reference this in a variable.
76
    $destination = drupal_get_destination();
77
    if (webform_submission_access($node, $submission, 'edit', $user)) {
78
      $row[] = l(t('Edit'), "node/$node->nid/submission/$sid/edit", array('query' => $destination));
79
      $operation_count++;
80
    }
81
    if (webform_submission_access($node, $submission, 'delete', $user)) {
82
      $row[] = l(t('Delete'), "node/$node->nid/submission/$sid/delete", array('query' => $destination));
83
      $operation_count++;
84
    }
85
    if ($operation_count < $operation_total) {
86
      $row[count($row) - 1] = array('data' => $row[count($row) - 1], 'colspan' => $operation_total - $operation_count + 1);
87
    }
88
    $rows[] = $row;
89
  }
90

    
91
  $element['#theme'] = 'webform_results_submissions';
92
  $element['#node'] = $node;
93
  $element['#submissions'] = $submissions;
94
  $element['#total_count'] = $count;
95
  $element['#pager_count'] = $pager_count;
96
  $element['#attached']['library'][] = array('webform', 'admin');
97

    
98
  $element['table']['#theme'] = 'table';
99
  $element['table']['#header'] = $header;
100
  $element['table']['#rows'] = $rows;
101
  $element['table']['#operation_total'] = $operation_total;
102

    
103
  return $element;
104
}
105

    
106
/**
107
 * Returns the most appropriate view for this webform node.
108
 *
109
 * Site builders can customize the view that webform uses by webform node id or
110
 * by webform content type. For example, use webform_results_123 to for node
111
 * id 123 or webform_results_my_content_type for a node of type my_content_type.
112
 *
113
 * @param object $node
114
 *   Loaded webform node.
115
 * @param string $view_id
116
 *   The machine_id of the view, such as webform_results or webform_submissions.
117
 *
118
 * @return object|null
119
 *   The loaded view.
120
 */
121
function webform_get_view($node, $view_id) {
122
  foreach (array("{$view_id}_{$node->nid}", "{$view_id}_{$node->type}", $view_id) as $id) {
123
    $view = views_get_view($id);
124
    if ($view) {
125
      return $view;
126
    }
127
  }
128
}
129

    
130
/**
131
 * Theme the list of links for selecting the number of results per page.
132
 *
133
 * @param array $variables
134
 *   Array with keys:
135
 *     - "total_count": The total number of results available.
136
 *     - "pager_count": The current number of results displayed per page.
137
 *
138
 * @return string
139
 *   Pager.
140
 */
141
function theme_webform_results_per_page(array $variables) {
142
  $total_count = $variables['total_count'];
143
  $pager_count = $variables['pager_count'];
144
  $output = '';
145

    
146
  // Create a list of results-per-page options.
147
  $counts = array(
148
    '20' => '20',
149
    '50' => '50',
150
    '100' => '100',
151
    '200' => '200',
152
    '500' => '500',
153
    '1000' => '1000',
154
    '0' => t('All'),
155
  );
156

    
157
  $count_links = array();
158

    
159
  foreach ($counts as $number => $text) {
160
    if ($number < $total_count) {
161
      $count_links[] = l($text, $_GET['q'], array('query' => array('results' => $number), 'attributes' => array('class' => array($pager_count == $number ? 'selected' : ''))));
162
    }
163
  }
164

    
165
  $output .= '<div class="webform-results-per-page">';
166
  if (count($count_links) > 1) {
167
    $output .= t('Show !count results per page.', array('!count' => implode(' | ', $count_links)));
168
  }
169
  else {
170
    $output .= t('Showing all results.');
171
  }
172
  if ($total_count > 1) {
173
    $output .= ' ' . t('@total results total.', array('@total' => $total_count));
174
  }
175
  $output .= '</div>';
176

    
177
  return $output;
178
}
179

    
180
/**
181
 * Theme the header of the submissions table.
182
 *
183
 * This is done in it's own function so that webform can retrieve the header and
184
 * use it for sorting the results.
185
 */
186
function theme_webform_results_submissions_header($variables) {
187
  $node = $variables['node'];
188

    
189
  $columns = array(
190
    array('data' => t('#'), 'field' => 'sid', 'sort' => 'desc'),
191
    array('data' => t('Submitted'), 'field' => 'submitted'),
192
  );
193
  if (webform_results_access($node)) {
194
    $columns[] = array('data' => t('User'), 'field' => 'name');
195
    $columns[] = array('data' => t('IP Address'), 'field' => 'remote_addr');
196
  }
197
  $columns[] = array('data' => t('Operations'), 'colspan' => module_exists('print') ? 5 : 3);
198

    
199
  return $columns;
200
}
201

    
202
/**
203
 * Preprocess function for webform-results-submissions.tpl.php.
204
 */
205
function template_preprocess_webform_results_submissions(&$vars) {
206
  $vars['node'] = $vars['element']['#node'];
207
  $vars['submissions'] = $vars['element']['#submissions'];
208
  $vars['table'] = $vars['element']['table'];
209
  $vars['total_count'] = $vars['element']['#total_count'];
210
  $vars['pager_count'] = $vars['element']['#pager_count'];
211
  $vars['is_submissions'] = (arg(2) == 'submissions') ? 1 : 0;
212

    
213
  unset($vars['element']);
214
}
215

    
216
/**
217
 * Create a table containing all submitted values for a webform node.
218
 */
219
function webform_results_table($node, $pager_count = 0) {
220

    
221
  // Determine whether views or hard-coded tables should be used for the
222
  // submissions table.
223
  if (!webform_variable_get('webform_table')) {
224
    // Load and preview the results view with a node id argument.
225
    $view = webform_get_view($node, 'webform_results');
226
    return $view->preview('default', array($node->nid));
227
  }
228

    
229
  // Get all the submissions for the node.
230
  if (isset($_GET['results']) && is_numeric($_GET['results'])) {
231
    $pager_count = $_GET['results'];
232
  }
233

    
234
  // Get all the submissions for the node.
235
  $header = theme('webform_results_table_header', array('node' => $node));
236
  $submissions = webform_get_submissions($node->nid, $header, $pager_count);
237
  $total_count = webform_get_submission_count($node->nid, NULL, NULL);
238

    
239
  $output[] = array(
240
    '#theme' => 'webform_results_table',
241
    '#node' => $node,
242
    '#components' => $node->webform['components'],
243
    '#submissions' => $submissions,
244
    '#total_count' => $total_count,
245
    '#pager_count' => $pager_count,
246
  );
247
  if ($pager_count) {
248
    $output[] = array('#theme' => 'pager');
249
  }
250
  return $output;
251
}
252

    
253
/**
254
 * Theme function for the Webform results table header.
255
 */
256
function theme_webform_results_table_header($variables) {
257
  return array(
258
    array('data' => t('#'), 'field' => 'sid', 'sort' => 'desc'),
259
    array('data' => t('Submitted'), 'field' => 'submitted'),
260
    array('data' => t('User'), 'field' => 'name'),
261
    array('data' => t('IP Address'), 'field' => 'remote_addr'),
262
  );
263
}
264

    
265
/**
266
 * Theme the results table displaying all the submissions for a particular node.
267
 *
268
 * @param array $variables
269
 *   An array with keys:
270
 *    - node: The node whose results are being displayed.
271
 *    - components: An associative array of the components for this webform.
272
 *    - submissions: An array of all submissions for this webform.
273
 *    - total_count: The total number of submissions to this webform.
274
 *    - pager_count: The number of results to be shown per page.
275
 *
276
 * @return string
277
 *   HTML string with result data.
278
 */
279
function theme_webform_results_table(array $variables) {
280
  drupal_add_library('webform', 'admin');
281

    
282
  $node = $variables['node'];
283
  $components = $variables['components'];
284
  $submissions = $variables['submissions'];
285
  $total_count = $variables['total_count'];
286
  $pager_count = $variables['pager_count'];
287

    
288
  $rows = array();
289
  $cell = array();
290

    
291
  // This header has to be generated separately so we can add the SQL necessary.
292
  // to sort the results.
293
  $header = theme('webform_results_table_header', array('node' => $node));
294

    
295
  // Generate a row for each submission.
296
  foreach ($submissions as $sid => $submission) {
297
    $link_text = $submission->is_draft ? t('@serial (draft)', array('@serial' => $submission->serial)) : $submission->serial;
298
    $cell[] = l($link_text, 'node/' . $node->nid . '/submission/' . $sid);
299
    $cell[] = format_date($submission->submitted, 'short');
300
    $cell[] = theme('username', array('account' => $submission));
301
    $cell[] = $submission->remote_addr;
302
    $component_headers = array();
303

    
304
    // Generate a cell for each component.
305
    foreach ($components as $component) {
306
      $data = isset($submission->data[$component['cid']]) ? $submission->data[$component['cid']] : NULL;
307
      $submission_output = webform_component_invoke($component['type'], 'table', $component, $data);
308
      if ($submission_output !== NULL) {
309
        $component_headers[] = array('data' => check_plain($component['name']), 'field' => $component['cid']);
310
        $cell[] = $submission_output;
311
      }
312
    }
313

    
314
    $rows[] = $cell;
315
    unset($cell);
316
  }
317
  if (!empty($component_headers)) {
318
    $header = array_merge($header, $component_headers);
319
  }
320

    
321
  if (count($rows) == 0) {
322
    $rows[] = array(array('data' => t('There are no submissions for this form. <a href="!url">View this form</a>.', array('!url' => url('node/' . $node->nid))), 'colspan' => 4));
323
  }
324

    
325
  $output = '';
326
  $output .= theme('webform_results_per_page', array('total_count' => $total_count, 'pager_count' => $pager_count));
327
  $output .= theme('table', array('header' => $header, 'rows' => $rows));
328
  return $output;
329
}
330

    
331
/**
332
 * Delete all submissions for a node.
333
 *
334
 * @param $nid
335
 *   The node id whose submissions will be deleted.
336
 * @param $batch_size
337
 *   The number of submissions to be processed. NULL means all submissions.
338
 *
339
 * @return int
340
 *   The number of submissions processed.
341
 */
342
function webform_results_clear($nid, $batch_size = NULL) {
343
  $node = node_load($nid);
344
  $submissions = webform_get_submissions($nid, NULL, $batch_size);
345
  $count = 0;
346
  foreach ($submissions as $submission) {
347
    webform_submission_delete($node, $submission);
348
    $count++;
349
  }
350
  return $count;
351
}
352

    
353
/**
354
 * Confirmation form to delete all submissions for a node.
355
 *
356
 * @param $nid
357
 *   ID of node for which to clear submissions.
358
 */
359
function webform_results_clear_form($form, $form_state, $node) {
360
  drupal_set_title(t('Clear Form Submissions'));
361

    
362
  $form = array();
363
  $form['nid'] = array('#type' => 'value', '#value' => $node->nid);
364
  $question = t('Are you sure you want to delete all submissions for this form?');
365

    
366
  return confirm_form($form, $question, 'node/' . $node->nid . '/webform-results', NULL, t('Clear'), t('Cancel'));
367
}
368

    
369
/**
370
 * Form submit handler.
371
 */
372
function webform_results_clear_form_submit($form, &$form_state) {
373
  $nid = $form_state['values']['nid'];
374
  $node = node_load($nid);
375
  // Set a modest batch size, due to the inefficiency of the hooks invoked when
376
  // submissions are deleted.
377
  $batch_size = min(webform_export_batch_size($node), 500);
378

    
379
  // Set up a batch to clear the results.
380
  $batch = array(
381
    'operations' => array(
382
      array('webform_clear_batch_rows', array($node, $batch_size)),
383
    ),
384
    'finished' => 'webform_clear_batch_finished',
385
    'title' => t('Clear submissions'),
386
    'init_message' => t('Clearing submission data'),
387
    'error_message' => t('The submissions could not be cleared because an error occurred.'),
388
    'file' => drupal_get_path('module', 'webform') . '/includes/webform.report.inc',
389
  );
390
  batch_set($batch);
391
  $form_state['redirect'] = 'node/' . $nid . '/webform-results';
392
}
393

    
394
/**
395
 * Batch API callback; Write the rows of the export to the export file.
396
 */
397
function webform_clear_batch_rows($node, $batch_size, &$context) {
398
  // Initialize the results if this is the first execution of the batch
399
  // operation.
400
  if (!isset($context['results']['count'])) {
401
    $context['results'] = array(
402
      'count' => 0,
403
      'total' => webform_get_submission_count($node->nid),
404
      'node' => $node,
405
    );
406
  }
407

    
408
  // Clear a batch of submissions.
409
  $count = webform_results_clear($node->nid, $batch_size);
410
  $context['results']['count'] += $count;
411

    
412
  // Display status message.
413
  $context['message'] = t('Cleared @count of @total submissions...', array('@count' => $context['results']['count'], '@total' => $context['results']['total']));
414
  $context['finished'] = $count > 0
415
                            ? $context['results']['count'] / $context['results']['total']
416
                            : 1.0;
417
}
418

    
419
/**
420
 * Batch API completion callback; Finish clearing submissions.
421
 */
422
function webform_clear_batch_finished($success, $results, $operations) {
423
  if ($success) {
424
    $title = $results['node']->title;
425
    drupal_set_message(t('Webform %title entries cleared.', array('%title' => $title)));
426
    watchdog('webform', 'Webform %title entries cleared.', array('%title' => $title));
427
  }
428
}
429

    
430
/**
431
 * Form to configure the download of CSV files.
432
 */
433
function webform_results_download_form($form, &$form_state, $node) {
434
  module_load_include('inc', 'webform', 'includes/webform.export');
435
  module_load_include('inc', 'webform', 'includes/webform.components');
436

    
437
  $form['#attached']['js'][] = drupal_get_path('module', 'webform') . '/js/webform-admin.js';
438

    
439
  // If an export is waiting to be downloaded, redirect the user there after
440
  // the page has finished loading.
441
  if (isset($_SESSION['webform_export_info'])) {
442
    $download_url = url('node/' . $node->nid . '/webform-results/download-file', array('absolute' => TRUE));
443
    $form['#attached']['js'][] = array('data' => array('webformExport' => $download_url), 'type' => 'setting');
444
  }
445

    
446
  $form['node'] = array(
447
    '#type' => 'value',
448
    '#value' => $node,
449
  );
450

    
451
  $form['format'] = array(
452
    '#type' => 'radios',
453
    '#title' => t('Export format'),
454
    '#options' => webform_export_list(),
455
    '#default_value' => webform_variable_get('webform_export_format'),
456
  );
457

    
458
  $form['delimited_options'] = array(
459
    '#type' => 'container',
460
    'warning' => array(
461
      '#markup' => '<p>' .
462
      t('<strong>Warning:</strong> Opening delimited text files with spreadsheet applications may expose you to <a href="!link">formula injection</a> or other security vulnerabilities. When the submissions contain data from untrusted users and the downloaded file will be used with spreadsheets, use Microsoft Excel format.',
463
        array('!link' => url('https://www.google.com/search?q=spreadsheet+formula+injection'))) .
464
      '</p>',
465
    ),
466
    'delimiter' => array(
467
      '#type' => 'select',
468
      '#title' => t('Delimited text format'),
469
      '#description' => t('This is the delimiter used in the CSV/TSV file when downloading Webform results. Using tabs in the export is the most reliable method for preserving non-latin characters. You may want to change this to another character depending on the program with which you anticipate importing results.'),
470
      '#default_value' => webform_variable_get('webform_csv_delimiter'),
471
      '#options' => array(
472
        ','  => t('Comma (,)'),
473
        '\t' => t('Tab (\t)'),
474
        ';'  => t('Semicolon (;)'),
475
        ':'  => t('Colon (:)'),
476
        '|'  => t('Pipe (|)'),
477
        '.'  => t('Period (.)'),
478
        ' '  => t('Space ( )'),
479
      ),
480
    ),
481
    '#states' => array(
482
      'visible' => array(
483
        ':input[name=format]' => array('value' => 'delimited'),
484
      ),
485
    ),
486
  );
487

    
488
  $form['header_keys'] = array(
489
    '#type' => 'radios',
490
    '#title' => t('Column header format'),
491
    '#options' => array(
492
      -1 => t('None'),
493
      0 => t('Label'),
494
      1 => t('Form Key'),
495
    ),
496
    '#default_value' => 0,
497
    '#description' => t('Choose whether to show the label or form key in each column header.'),
498
  );
499

    
500
  $form['select_options'] = array(
501
    '#type' => 'fieldset',
502
    '#title' => t('Select list options'),
503
    '#collapsible' => TRUE,
504
    '#collapsed' => TRUE,
505
  );
506

    
507
  $form['select_options']['select_keys'] = array(
508
    '#type' => 'radios',
509
    '#title' => t('Select keys'),
510
    '#options' => array(
511
      0 => t('Full, human-readable options (values)'),
512
      1 => t('Short, raw options (keys)'),
513
    ),
514
    '#default_value' => 0,
515
    '#description' => t('Choose which part of options should be displayed from key|value pairs.'),
516
  );
517

    
518
  $form['select_options']['select_format'] = array(
519
    '#type' => 'radios',
520
    '#title' => t('Select list format'),
521
    '#options' => array(
522
      'separate' => t('Separate'),
523
      'compact' => t('Compact'),
524
    ),
525
    '#default_value' => 'separate',
526
    '#attributes' => array('class' => array('webform-select-list-format')),
527
    '#theme' => 'webform_results_download_select_format',
528
  );
529

    
530
  $csv_components = array('info' => t('Submission information'));
531
  // Prepend information fields with "-" to indent.
532
  foreach (webform_results_download_submission_information($node) as $key => $title) {
533
    $csv_components[$key] = '-' . $title;
534
  }
535

    
536
  $csv_components += webform_component_list($node, 'csv', TRUE);
537

    
538
  $form['components'] = array(
539
    '#type' => 'select',
540
    '#title' => t('Included export components'),
541
    '#options' => $csv_components,
542
    '#default_value' => array_keys($csv_components),
543
    '#multiple' => TRUE,
544
    '#size' => 10,
545
    '#description' => t('The selected components will be included in the export.'),
546
    '#process' => array('webform_component_select'),
547
  );
548

    
549
  $form['range'] = array(
550
    '#type' => 'fieldset',
551
    '#title' => t('Download range options'),
552
    '#collapsible' => TRUE,
553
    '#collapsed' => TRUE,
554
    '#tree' => TRUE,
555
    '#theme' => 'webform_results_download_range',
556
    '#element_validate' => array('webform_results_download_range_validate'),
557
    '#after_build' => array('webform_results_download_range_after_build'),
558
  );
559

    
560
  $form['range']['range_type'] = array(
561
    '#type' => 'radios',
562
    '#options' => array(
563
      'all' => t('All submissions'),
564
      'new' => t('Only new submissions since your last download'),
565
      'latest' => t('Only the latest'),
566
      'range_serial' => t('All submissions starting from'),
567
      'range_date' => t('All submissions by date'),
568
    ),
569
    '#default_value' => 'all',
570
  );
571
  $form['range']['latest'] = array(
572
    '#type' => 'textfield',
573
    '#size' => 5,
574
    '#maxlength' => 8,
575
  );
576
  $form['range']['start'] = array(
577
    '#type' => 'textfield',
578
    '#size' => 5,
579
    '#maxlength' => 8,
580
  );
581
  $form['range']['end'] = array(
582
    '#type' => 'textfield',
583
    '#size' => 5,
584
    '#maxlength' => 8,
585
  );
586
  $date_attributes = array('placeholder' => format_date(REQUEST_TIME, 'custom', webform_date_format('short')));
587
  $form['range']['start_date'] = array(
588
    '#type' => 'textfield',
589
    '#size' => 20,
590
    '#attributes' => $date_attributes,
591
  );
592
  $form['range']['end_date'] = array(
593
    '#type' => 'textfield',
594
    '#size' => 20,
595
    '#attributes' => $date_attributes,
596
  );
597

    
598
  // If drafts are allowed, provide options to filter download based on draft
599
  // status.
600
  $form['range']['completion_type'] = array(
601
    '#type' => 'radios',
602
    '#title' => t('Included submissions'),
603
    '#default_value' => 'all',
604
    '#options' => array(
605
      'all' => t('Finished and draft submissions'),
606
      'finished' => t('Finished submissions only'),
607
      'draft' => t('Drafts only'),
608
    ),
609
    '#access' => ($node->webform['allow_draft'] || $node->webform['auto_save']),
610
  );
611

    
612
  // By default results are downloaded. User can override this value if
613
  // programmatically submitting this form.
614
  $form['download'] = array(
615
    '#type' => 'value',
616
    '#default_value' => TRUE,
617
  );
618

    
619
  $form['actions'] = array('#type' => 'actions');
620
  $form['actions']['submit'] = array(
621
    '#type' => 'submit',
622
    '#value' => t('Download'),
623
  );
624

    
625
  return $form;
626
}
627

    
628
/**
629
 * FormAPI element validate function for the range fieldset.
630
 */
631
function webform_results_download_range_validate($element, $form_state) {
632
  switch ($element['range_type']['#value']) {
633
    case 'latest':
634
      // Download latest x submissions.
635
      if ($element['latest']['#value'] == '') {
636
        form_error($element['latest'], t('Latest number of submissions field is required.'));
637
      }
638
      else {
639
        if (!is_numeric($element['latest']['#value'])) {
640
          form_error($element['latest'], t('Latest number of submissions must be numeric.'));
641
        }
642
        else {
643
          if ($element['latest']['#value'] <= 0) {
644
            form_error($element['latest'], t('Latest number of submissions must be greater than 0.'));
645
          }
646
        }
647
      }
648
      break;
649

    
650
    case 'range_serial':
651
      // Download Start-End range of submissions.
652
      // Start submission number.
653
      if ($element['start']['#value'] == '') {
654
        form_error($element['start'], t('Start submission number is required.'));
655
      }
656
      else {
657
        if (!is_numeric($element['start']['#value'])) {
658
          form_error($element['start'], t('Start submission number must be numeric.'));
659
        }
660
        else {
661
          if ($element['start']['#value'] <= 0) {
662
            form_error($element['start'], t('Start submission number must be greater than 0.'));
663
          }
664
        }
665
      }
666
      // End submission number.
667
      if ($element['end']['#value'] != '') {
668
        if (!is_numeric($element['end']['#value'])) {
669
          form_error($element['end'], t('End submission number must be numeric.'));
670
        }
671
        else {
672
          if ($element['end']['#value'] <= 0) {
673
            form_error($element['end'], t('End submission number must be greater than 0.'));
674
          }
675
          else {
676
            if ($element['end']['#value'] < $element['start']['#value']) {
677
              form_error($element['end'], t('End submission number must not be less than Start submission number.'));
678
            }
679
          }
680
        }
681
      }
682
      break;
683

    
684
    case 'range_date':
685
      // Download Start-end range of submissions.
686
      // Start submission time.
687
      $format = webform_date_format('short');
688
      $start_date = DateTime::createFromFormat($format, $element['start_date']['#value']);
689
      if ($element['start_date']['#value'] == '') {
690
        form_error($element['start_date'], t('Start date range is required.'));
691
      }
692
      elseif ($start_date === FALSE) {
693
        form_error($element['start_date'], t('Start date range is not in a valid format.'));
694
      }
695
      // End submission time.
696
      $end_date = DateTime::createFromFormat($format, $element['end_date']['#value']);
697
      if ($element['end_date']['#value'] != '') {
698
        if ($end_date === FALSE) {
699
          form_error($element['end_date'], t('End date range is not in a valid format.'));
700
        }
701
        elseif ($start_date !== FALSE && $start_date > $end_date) {
702
          form_error($element['end_date'], t('End date range must not be before the Start date.'));
703
        }
704
      }
705
      break;
706
  }
707

    
708
  // Check that the range will return something at all.
709
  $range_options = array(
710
    'range_type' => $element['range_type']['#value'],
711
    'start' => $element['start']['#value'],
712
    'end' => $element['end']['#value'],
713
    'latest' => $element['latest']['#value'],
714
    'start_date' => $element['start_date']['#value'],
715
    'end_date' => $element['end_date']['#value'],
716
    'completion_type' => $element['completion_type']['#value'],
717
    'batch_size' => 1,
718
    'batch_number' => 0,
719
  );
720
  if (!form_get_errors() && !webform_download_sids_count($form_state['values']['node']->nid, $range_options)) {
721
    form_error($element['range_type'], t('The specified range will not return any results.'));
722
  }
723

    
724
}
725

    
726
/**
727
 * FormAPI after build function for the download range fieldset.
728
 */
729
function webform_results_download_range_after_build($element, &$form_state) {
730
  $node = $form_state['values']['node'];
731

    
732
  // Build a list of counts of new and total submissions.
733
  $last_download = webform_download_last_download_info($node->nid);
734

    
735
  $element['#webform_download_info']['sid'] = $last_download ? $last_download['sid'] : 0;
736
  $element['#webform_download_info']['serial'] = $last_download ? $last_download['serial'] : NULL;
737
  $element['#webform_download_info']['requested'] = $last_download ? $last_download['requested'] : $node->created;
738
  $element['#webform_download_info']['total'] = webform_get_submission_count($node->nid, NULL, NULL);
739
  $element['#webform_download_info']['new'] = webform_download_sids_count($node->nid, array('range_type' => 'new', 'completion_type' => 'finished'));
740
  if ($node->webform['allow_draft'] || $node->webform['auto_save']) {
741
    $element['#webform_download_info']['new_draft'] = webform_download_sids_count($node->nid, array('range_type' => 'new', 'completion_type' => 'draft'));
742
  }
743

    
744
  return $element;
745
}
746

    
747
/**
748
 * Theme the output of the export range fieldset.
749
 */
750
function theme_webform_results_download_range($variables) {
751
  drupal_add_library('webform', 'admin');
752

    
753
  $element = $variables['element'];
754
  $download_info = $element['#webform_download_info'];
755

    
756
  // Set description for total of all submissions.
757
  $element['range_type']['all']['#theme_wrappers'] = array('webform_inline_radio');
758
  $element['range_type']['all']['#title'] .= ' (' . t('@count total', array('@count' => $download_info['total'])) . ')';
759

    
760
  // Set description for "New submissions since last download".
761
  $format = webform_date_format('short');
762
  $requested_date = format_date($download_info['requested'], 'custom', $format);
763
  $element['range_type']['new']['#theme_wrappers'] = array('webform_inline_radio');
764
  if (isset($download_info['new_draft'])) {
765
    $args = array(
766
      '@new_draft' => $download_info['new_draft'],
767
      '@new' => $download_info['new'],
768
      '@date' => $requested_date,
769
    );
770
    $element['range_type']['new']['#title'] .= ' (' . t('@new_draft new drafts, @new new finished since @date', $args) . ')';
771
    $total_count = $download_info['new_draft'] + $download_info['new'];
772
  }
773
  else {
774
    $args = array(
775
      '@count' => $download_info['new'],
776
      '@date' => $requested_date,
777
    );
778
    $element['range_type']['new']['#title'] .= ' (' . t('@count new since @date', $args) . ')';
779
    $total_count = $download_info['new'];
780
  }
781

    
782
  // Disable option if there are no new submissions.
783
  if ($total_count == 0) {
784
    $element['range_type']['new']['#attributes']['disabled'] = 'disabled';
785
  }
786

    
787
  // Render latest x submissions option.
788
  $element['latest']['#attributes']['class'][] = 'webform-set-active';
789
  $element['latest']['#theme_wrappers'] = array();
790
  $element['range_type']['latest']['#theme_wrappers'] = array('webform_inline_radio');
791
  $element['range_type']['latest']['#title'] = t('Only the latest !number submissions', array('!number' => drupal_render($element['latest'])));
792

    
793
  // Render Start-End submissions option.
794
  $element['start']['#attributes']['class'][] = 'webform-set-active';
795
  $element['end']['#attributes']['class'][] = 'webform-set-active';
796
  $element['start']['#theme_wrappers'] = array();
797
  $element['end']['#theme_wrappers'] = array();
798
  $element['start_date']['#attributes']['class'] = array('webform-set-active');
799
  $element['end_date']['#attributes']['class'] = array('webform-set-active');
800
  $element['start_date']['#theme_wrappers'] = array();
801
  $element['end_date']['#theme_wrappers'] = array();
802
  $element['range_type']['range_serial']['#theme_wrappers'] = array('webform_inline_radio');
803
  $last_serial = $download_info['serial'] ? $download_info['serial'] : drupal_placeholder(t('none'));
804
  $element['range_type']['range_serial']['#title'] = t('Submissions by number from !start and optionally to: !end &nbsp; (Last downloaded: !serial)',
805
    array('!start' => drupal_render($element['start']), '!end' => drupal_render($element['end']), '!serial' => $last_serial));
806

    
807
  // Date range.
808
  $element['range_type']['range_date']['#theme_wrappers'] = array('webform_inline_radio');
809
  $element['range_type']['range_date']['#title'] = t('Submissions by date from !start_date and optionally to: !end_date', array('!start_date' => drupal_render($element['start_date']), '!end_date' => drupal_render($element['end_date'])));
810

    
811
  return drupal_render_children($element);
812
}
813

    
814
/**
815
 * Theme the output of the select list format radio buttons.
816
 */
817
function theme_webform_results_download_select_format($variables) {
818
  drupal_add_library('webform', 'admin');
819

    
820
  $element = $variables['element'];
821
  $output = '';
822

    
823
  // Build an example table for the separate option.
824
  $header = array(t('Option A'), t('Option B'), t('Option C'));
825
  $rows = array(
826
    array('X', '', ''),
827
    array('X', '', 'X'),
828
    array('', 'X', 'X'),
829
  );
830

    
831
  $element['separate']['#attributes']['class'] = array();
832
  $element['separate']['#description'] = theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE));
833
  $element['separate']['#description'] .= t('Separate options are more suitable for building reports, graphs, and statistics in a spreadsheet application.');
834
  $output .= drupal_render($element['separate']);
835

    
836
  // Build an example table for the compact option.
837
  $header = array(t('My select list'));
838
  $rows = array(
839
    array('Option A'),
840
    array('Option A,Option C'),
841
    array('Option B,Option C'),
842
  );
843

    
844
  $element['compact']['#attributes']['class'] = array();
845
  $element['compact']['#description'] = theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE));
846
  $element['compact']['#description'] .= t('Compact options are more suitable for importing data into other systems.');
847
  $output .= drupal_render($element['compact']);
848

    
849
  return $output;
850
}
851

    
852
/**
853
 * Submit handler for webform_results_download_form().
854
 */
855
function webform_results_download_form_submit(&$form, &$form_state) {
856
  $node = $form_state['values']['node'];
857
  $format = $form_state['values']['format'];
858

    
859
  $options = array(
860
    'delimiter' => $form_state['values']['delimiter'],
861
    'components' => array_keys(array_filter($form_state['values']['components'])),
862
    'header_keys' => $form_state['values']['header_keys'],
863
    'select_keys' => $form_state['values']['select_keys'],
864
    'select_format' => $form_state['values']['select_format'],
865
    'range' => $form_state['values']['range'],
866
    'download' => $form_state['values']['download'],
867
  );
868

    
869
  $defaults = webform_results_download_default_options($node, $format);
870
  $options += $defaults;
871
  $options['range'] += $defaults['range'];
872

    
873
  // Determine an appropriate batch size based on the form and server specs.
874
  if (!isset($options['range']['batch_size'])) {
875
    $options['range']['batch_size'] = webform_export_batch_size($node);
876
  }
877

    
878
  $options['file_name'] = _webform_export_tempname();
879

    
880
  // Set up a batch to export the results.
881
  $batch = webform_results_export_batch($node, $format, $options);
882
  batch_set($batch);
883
}
884

    
885
/**
886
 * Calculate an appropriate batch size for bulk submission operations.
887
 *
888
 * @param object $node
889
 *   The webform node.
890
 *
891
 * @return int
892
 *   The number of submissions to be processed at once.
893
 */
894
function webform_export_batch_size($node) {
895
  // Start the batch size at 50,000 per batch, but divide by number of
896
  // components in the form. For example, if a form had 10 components, it would
897
  // export 5,000 submissions at a time.
898
  $batch_size = ceil(50000 / max(1, count($node->webform['components'])));
899

    
900
  // Every 32MB of additional memory after 64MB adds a multiplier in size.
901
  $memory_limit = parse_size(ini_get('memory_limit'));
902
  $mb = 1048576;
903
  $memory_modifier = max(1, ($memory_limit - (64 * $mb)) / (32 * $mb));
904
  $batch_size = ceil($batch_size * $memory_modifier);
905

    
906
  // For time reasons, limit the batch size to 5,000.
907
  $batch_size = min($batch_size, 5000);
908

    
909
  // Allow a non-UI configuration to override the batch size.
910
  $batch_size = variable_get('webform_export_batch_size', $batch_size);
911

    
912
  return $batch_size;
913
}
914

    
915
/**
916
 * Returns a temporary export filename.
917
 */
918
function _webform_export_tempname() {
919
  $webform_export_path = variable_get('webform_export_path', 'temporary://');
920

    
921
  // If the directory does not exist, create it.
922
  file_prepare_directory($webform_export_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
923
  return drupal_tempnam($webform_export_path, 'webform_');
924
}
925

    
926
/**
927
 * Generate a Excel-readable CSV file containing all submissions for a Webform.
928
 *
929
 * @deprecated in webform:7.x-4.8 and is removed from webform:7.x-5.0. Use
930
 * webform_results_export_batch().
931
 * @see https://www.drupal.org/project/webform/issues/2465291
932
 *
933
 * @return array|null
934
 *   The array of export info or null if the file could not be opened.
935
 */
936
function webform_results_export($node, $format = 'delimited', $options = array()) {
937
  module_load_include('inc', 'webform', 'includes/webform.export');
938
  module_load_include('inc', 'webform', 'includes/webform.components');
939

    
940
  $defaults = webform_results_download_default_options($node, $format);
941
  $options += $defaults;
942
  $options['range'] += $defaults['range'];
943

    
944
  // Open a new Webform exporter object.
945
  $exporter = webform_export_create_handler($format, $options);
946

    
947
  $file_name = _webform_export_tempname();
948
  $handle = fopen($file_name, 'w');
949
  if (!$handle) {
950
    return;
951
  }
952

    
953
  // Add the beginning of file marker (little-endian usually for MS Excel).
954
  $exporter->bof($handle);
955

    
956
  // Add headers to the file.
957
  $row_count = 0;
958
  $col_count = 0;
959
  $headers = webform_results_download_headers($node, $options);
960
  foreach ($headers as $row) {
961
    // Output header if header_keys is non-negative. -1 means no headers.
962
    if ($options['header_keys'] >= 0) {
963
      $exporter->add_row($handle, $row, $row_count);
964
      $row_count++;
965
    }
966
    $col_count = count($row) > $col_count ? count($row) : $col_count;
967
  }
968

    
969
  // Write data from submissions. $last_is is non_NULL to trigger returning it
970
  // by reference.
971
  $last_sid = TRUE;
972
  $rows = webform_results_download_rows($node, $options, 0, $last_sid);
973
  foreach ($rows as $row) {
974
    $exporter->add_row($handle, $row, $row_count);
975
    $row_count++;
976
  }
977

    
978
  // Add the closing bytes.
979
  $exporter->eof($handle, $row_count, $col_count);
980

    
981
  // Close the file.
982
  @fclose($handle);
983

    
984
  $export_info['format'] = $format;
985
  $export_info['options'] = $options;
986
  $export_info['file_name'] = $file_name;
987
  $export_info['row_count'] = $row_count;
988
  $export_info['col_count'] = $col_count;
989
  $export_info['last_sid'] = $last_sid;
990
  $export_info['exporter'] = $exporter;
991

    
992
  return $export_info;
993
}
994

    
995
/**
996
 * Return a Batch API array of commands that will generate an export.
997
 *
998
 * @param $node
999
 *   The webform node on which to generate the analysis.
1000
 * @param string $format
1001
 *   (optional) Delimiter of the exported file.
1002
 * @param array $options
1003
 *   (optional) An associative array of options that define the output format.
1004
 *   These are generally passed through from the GUI interface. Possible options
1005
 *   include:
1006
 *   - sids: An array of submission IDs to which this export may be filtered.
1007
 *     May be used to generate exports that are per-user or other groups of
1008
 *     submissions.
1009
 *
1010
 * @return array
1011
 *   A Batch API array suitable to pass to batch_set().
1012
 */
1013
function webform_results_export_batch($node, $format = 'delimited', array $options = array()) {
1014
  $defaults = webform_results_download_default_options($node, $format);
1015
  $options += $defaults;
1016
  $options['range'] += $defaults['range'];
1017

    
1018
  return array(
1019
    'operations' => array(
1020
      array('webform_results_batch_bof', array($node, $format, $options)),
1021
      array('webform_results_batch_headers', array($node, $format, $options)),
1022
      array('webform_results_batch_rows', array($node, $format, $options)),
1023
      array('webform_results_batch_eof', array($node, $format, $options)),
1024
      array('webform_results_batch_post_process', array($node, $format, $options)),
1025
      array('webform_results_batch_results', array($node, $format, $options)),
1026
    ),
1027
    'finished' => 'webform_results_batch_finished',
1028
    'title' => t('Exporting submissions'),
1029
    'init_message' => t('Creating export file'),
1030
    'error_message' => t('The export file could not be created because an error occurred.'),
1031
    'file' => drupal_get_path('module', 'webform') . '/includes/webform.report.inc',
1032
  );
1033
}
1034

    
1035
/**
1036
 * Print the header rows for the downloadable webform data.
1037
 *
1038
 * @param $node
1039
 *   The webform node on which to generate the analysis.
1040
 * @param array $options
1041
 *   A list of options that define the output format. These are generally passed
1042
 *   through from the GUI interface.
1043
 */
1044
function webform_results_download_headers($node, array $options) {
1045
  module_load_include('inc', 'webform', 'includes/webform.components');
1046
  $submission_information = webform_results_download_submission_information($node, $options);
1047

    
1048
  // Fill in the header for the submission information (if any).
1049
  $header[2] = $header[1] = $header[0] = count($submission_information) ? array_fill(0, count($submission_information), '') : array();
1050
  if (count($submission_information)) {
1051
    $header[0][0] = $node->title;
1052

    
1053
    if ($options['header_keys']) {
1054
      $header[1][0] = t('submission_details');
1055
      $submission_information_headers = array_keys($submission_information);
1056
    }
1057
    else {
1058
      $header[1][0] = t('Submission Details');
1059
      $submission_information_headers = array_values($submission_information);
1060
    }
1061
    foreach ($submission_information_headers as $column => $label) {
1062
      $header[2][$column] = $label;
1063
    }
1064
  }
1065

    
1066
  // Compile header information for components.
1067
  foreach ($options['components'] as $cid) {
1068
    if (isset($node->webform['components'][$cid])) {
1069
      $component = $node->webform['components'][$cid];
1070

    
1071
      // Let each component determine its headers.
1072
      if (webform_component_feature($component['type'], 'csv')) {
1073
        $component_header = (array) webform_component_invoke($component['type'], 'csv_headers', $component, $options);
1074
        // Allow modules to modify the component CSV header.
1075
        drupal_alter('webform_csv_header', $component_header, $component);
1076

    
1077
        // Merge component CSV header to overall CSV header.
1078
        $header[0] = array_merge($header[0], (array) $component_header[0]);
1079
        $header[1] = array_merge($header[1], (array) $component_header[1]);
1080
        $header[2] = array_merge($header[2], (array) $component_header[2]);
1081
      }
1082
    }
1083
  }
1084

    
1085
  return $header;
1086
}
1087

    
1088
/**
1089
 * Returns rows of downloadable webform data.
1090
 *
1091
 * @param $node
1092
 *   The webform node on which to generate the analysis.
1093
 * @param array $options
1094
 *   A list of options that define the output format. These are generally passed
1095
 *   through from the GUI interface.
1096
 * @param int $serial_start
1097
 *   The starting position for the Serial column in the output.
1098
 * @param $last_sid
1099
 *   If set to a non-NULL value, the last sid will be returned.
1100
 *
1101
 * @return array
1102
 *   An array of rows built according to the provided $serial_start and
1103
 *   $pager_count variables. Note that the current page number is determined
1104
 *   by the super-global $_GET['page'] variable.
1105
 *
1106
 * @deprecated in webform:7.x-4.8 and is removed from webform:7.x-5.0. See
1107
 * webform_results_download_rows_process().
1108
 * @see https://www.drupal.org/project/webform/issues/2465291
1109
 */
1110
function webform_results_download_rows($node, array $options, $serial_start = 0, &$last_sid = NULL) {
1111
  // Get all the required submissions for the download.
1112
  $filters['nid'] = $node->nid;
1113
  if (isset($options['sids'])) {
1114
    $filters['sid'] = $options['sids'];
1115
  }
1116
  elseif (!empty($options['completion_type']) && $options['completion_type'] !== 'all') {
1117
    $filters['is_draft'] = (int) ($options['completion_type'] === 'draft');
1118
  }
1119

    
1120
  $submissions = webform_get_submissions($filters, NULL);
1121

    
1122
  if (isset($last_sid)) {
1123
    $last_sid = end($submissions) ? key($submissions) : NULL;
1124
  }
1125

    
1126
  return webform_results_download_rows_process($node, $options, $serial_start, $submissions);
1127
}
1128

    
1129
/**
1130
 * Processes the submissions to be downloaded into exported rows.
1131
 *
1132
 * This is an internal routine and not intended for use by other modules.
1133
 *
1134
 * @param $node
1135
 *   The webform node on which to generate the analysis.
1136
 * @param array $options
1137
 *   A list of options that define the output format. These are generally passed
1138
 *   through from the GUI interface.
1139
 * @param $serial_start
1140
 *   The starting position for the Serial column in the output.
1141
 * @param array $submissions
1142
 *   An associative array of loaded submissions, indexed by sid.
1143
 *
1144
 * @return array
1145
 *   An array of rows built according to the provided $serial_start and
1146
 *   $pager_count variables. Note that the current page number is determined
1147
 *   by the super-global $_GET['page'] variable.
1148
 */
1149
function webform_results_download_rows_process($node, array $options, $serial_start, array $submissions) {
1150
  module_load_include('inc', 'webform', 'includes/webform.components');
1151

    
1152
  $submission_information = webform_results_download_submission_information($node, $options);
1153

    
1154
  // Generate a row for each submission.
1155
  $row_count = 0;
1156
  $rows = array();
1157
  foreach ($submissions as $sid => $submission) {
1158
    $row_count++;
1159

    
1160
    $row = array();
1161
    // Add submission information.
1162
    foreach (array_keys($submission_information) as $token) {
1163
      $cell = module_invoke_all('webform_results_download_submission_information_data', $token, $submission, $options, $serial_start, $row_count);
1164
      $context = array('token' => $token, 'submission' => $submission, 'options' => $options, 'serial_start' => $serial_start, 'row_count' => $row_count);
1165
      drupal_alter('webform_results_download_submission_information_data', $cell, $context);
1166
      // implode() to ensure everything from a single value goes into one
1167
      // column, even if more than one module responds to this item.
1168
      $row[] = implode(', ', $cell);
1169
    }
1170

    
1171
    foreach ($options['components'] as $cid) {
1172
      if (isset($node->webform['components'][$cid])) {
1173
        $component = $node->webform['components'][$cid];
1174
        // Let each component add its data.
1175
        $raw_data = isset($submission->data[$cid]) ? $submission->data[$cid] : NULL;
1176
        if (webform_component_feature($component['type'], 'csv')) {
1177
          $data = webform_component_invoke($component['type'], 'csv_data', $component, $options, $raw_data);
1178

    
1179
          // Allow modules to modify the CSV data.
1180
          drupal_alter('webform_csv_data', $data, $component, $submission);
1181

    
1182
          if (is_array($data)) {
1183
            $row = array_merge($row, array_values($data));
1184
          }
1185
          else {
1186
            $row[] = isset($data) ? $data : '';
1187
          }
1188
        }
1189
      }
1190
    }
1191

    
1192
    $rows[$serial_start + $row_count] = $row;
1193
  }
1194

    
1195
  return $rows;
1196
}
1197

    
1198
/**
1199
 * Default columns for submission information.
1200
 *
1201
 * By default all exports have several columns of generic information that
1202
 * applies to all submissions. This function returns the list of generic columns
1203
 * plus columns added by other modules.
1204
 *
1205
 * @param $options
1206
 *   Filter down the list of columns based on a provided column list.
1207
 *
1208
 * @return array
1209
 *   List of generic columns plus columns added by other modules
1210
 */
1211
function webform_results_download_submission_information($node, $options = array()) {
1212
  $submission_information = module_invoke_all('webform_results_download_submission_information_info');
1213
  drupal_alter('webform_results_download_submission_information_info', $submission_information);
1214

    
1215
  if (isset($options['components'])) {
1216
    foreach ($submission_information as $key => $label) {
1217
      if (!in_array($key, $options['components'])) {
1218
        unset($submission_information[$key]);
1219
      }
1220
    }
1221
  }
1222

    
1223
  return $submission_information;
1224
}
1225

    
1226
/**
1227
 * Implements hook_webform_results_download_submission_information_info().
1228
 */
1229
function webform_webform_results_download_submission_information_info() {
1230
  return array(
1231
    'webform_serial' => t('Serial'),
1232
    'webform_sid' => t('SID'),
1233
    'webform_time' => t('Submitted Time'),
1234
    'webform_completed_time' => t('Completed Time'),
1235
    'webform_modified_time' => t('Modified Time'),
1236
    'webform_draft' => t('Draft'),
1237
    'webform_ip_address' => t('IP Address'),
1238
    'webform_uid' => t('UID'),
1239
    'webform_username' => t('Username'),
1240
  );
1241
}
1242

    
1243
/**
1244
 * Implements hook_webform_results_download_submission_information_data().
1245
 */
1246
function webform_webform_results_download_submission_information_data($token, $submission, array $options, $serial_start, $row_count) {
1247
  switch ($token) {
1248
    case 'webform_serial':
1249
      return $submission->serial;
1250

    
1251
    case 'webform_sid':
1252
      return $submission->sid;
1253

    
1254
    case 'webform_time':
1255
      // Return timestamp in local time (not UTC).
1256
      if (!empty($options['iso8601_date'])) {
1257
        return format_date($submission->submitted, 'custom', 'Y-m-d\TH:i:s');
1258
      }
1259
      else {
1260
        return format_date($submission->submitted, 'short');
1261
      }
1262
    case 'webform_completed_time':
1263
      if (!$submission->completed) {
1264
        return '';
1265
      }
1266
      // Return timestamp in local time (not UTC).
1267
      elseif (!empty($options['iso8601_date'])) {
1268
        return format_date($submission->completed, 'custom', 'Y-m-d\TH:i:s');
1269
      }
1270
      else {
1271
        return format_date($submission->completed, 'short');
1272
      }
1273
    case 'webform_modified_time':
1274
      // Return timestamp in local time (not UTC).
1275
      if (!empty($options['iso8601_date'])) {
1276
        return format_date($submission->modified, 'custom', 'Y-m-d\TH:i:s');
1277
      }
1278
      else {
1279
        return format_date($submission->modified, 'short');
1280
      }
1281
    case 'webform_draft':
1282
      return $submission->is_draft;
1283

    
1284
    case 'webform_ip_address':
1285
      return $submission->remote_addr;
1286

    
1287
    case 'webform_uid':
1288
      return $submission->uid;
1289

    
1290
    case 'webform_username':
1291
      return $submission->name;
1292
  }
1293
}
1294

    
1295
/**
1296
 * Get options for creating downloadable versions of the webform data.
1297
 *
1298
 * @param $node
1299
 *   The webform node on which to generate the analysis.
1300
 * @param string $format
1301
 *   The export format being used.
1302
 *
1303
 * @return array
1304
 *   Option for creating downloadable version of the webform data.
1305
 */
1306
function webform_results_download_default_options($node, $format) {
1307
  $submission_information = webform_results_download_submission_information($node);
1308

    
1309
  $options = array(
1310
    'delimiter' => webform_variable_get('webform_csv_delimiter'),
1311
    'components' => array_merge(array_keys($submission_information), array_keys(webform_component_list($node, 'csv', TRUE))),
1312
    'header_keys' => 0,
1313
    'select_keys' => 0,
1314
    'select_format' => 'separate',
1315
    'range' => array(
1316
      'range_type' => 'all',
1317
      'completion_type' => 'all',
1318
    ),
1319
  );
1320

    
1321
  // Allow exporters to merge in additional options.
1322
  $exporter_information = webform_export_fetch_definition($format);
1323
  if (isset($exporter_information['options'])) {
1324
    $options = array_merge($options, $exporter_information['options']);
1325
  }
1326

    
1327
  return $options;
1328
}
1329

    
1330
/**
1331
 * Send a generated webform results file to the user's browser.
1332
 *
1333
 * @param $node
1334
 *   The webform node.
1335
 * @param $export_info
1336
 *   Export information array retrieved from webform_results_export().
1337
 */
1338
function webform_results_download($node, $export_info) {
1339
  // If the exporter provides a custom download method, use that.
1340
  if (method_exists($export_info['exporter'], 'download')) {
1341
    $export_info['exporter']->download($node, $export_info);
1342
  }
1343
  // Otherwise use the set_headers() method to set headers and then read in the
1344
  // file directly. Delete it when complete.
1345
  else {
1346
    $export_name = _webform_safe_name($node->title);
1347
    if (!strlen($export_name)) {
1348
      $export_name = t('Untitled');
1349
    }
1350
    $export_info['exporter']->set_headers($export_name);
1351
    ob_clean();
1352
    // The @ makes it silent.
1353
    @readfile($export_info['file_name']);
1354
    // Clean up, the @ makes it silent.
1355
    @unlink($export_info['file_name']);
1356
  }
1357

    
1358
  // Save the last exported sid for new-only exports.
1359
  webform_results_export_success($node, $export_info);
1360

    
1361
  exit();
1362
}
1363

    
1364
/**
1365
 * Save the last-exported sid for new-only exports.
1366
 *
1367
 * @param $node
1368
 *   The webform node.
1369
 * @param $export_info
1370
 *   Export information array retrieved from webform_results_export().
1371
 */
1372
function webform_results_export_success($node, $export_info) {
1373
  if (!in_array($export_info['options']['range']['range_type'], array('range', 'range_serial', 'range_date')) && !empty($export_info['last_sid'])) {
1374
    webform_update_webform_last_download($node, $GLOBALS['user']->uid, $export_info['last_sid'], REQUEST_TIME);
1375
  }
1376
}
1377

    
1378
/**
1379
 * Insert or update a download record in table {webform_last_download}.
1380
 *
1381
 * @param object $node
1382
 *   The Webform node of the record to update.
1383
 * @param int $uid
1384
 *   The user ID of the record to update.
1385
 * @param int $sid
1386
 *   The last downloaded submission number.
1387
 * @param int $requested
1388
 *   Timestamp of last download request.
1389
 *
1390
 * @return int
1391
 *   MergeQuery::STATUS_INSERT or ::STATUS_UPDATE.
1392
 */
1393
function webform_update_webform_last_download(stdClass $node, $uid, $sid, $requested) {
1394
  return db_merge('webform_last_download')
1395
    ->key(array(
1396
      'nid' => $node->nid,
1397
      'uid' => $uid,
1398
    ))
1399
    ->fields(array(
1400
      'sid' => $sid,
1401
      'requested' => $requested,
1402
    ))
1403
    ->execute();
1404
}
1405

    
1406
/**
1407
 * Menu callback; Download an exported file.
1408
 *
1409
 * This callabck requires that an export file be already generated by a batch
1410
 * process. The $_SESSION settings are usually put in place by the
1411
 * webform_results_export_results() function.
1412
 *
1413
 * @param $node
1414
 *   The webform $node whose export file is being downloaded.
1415
 *
1416
 * @return null|string
1417
 *   Either an export file is downloaded with the appropriate HTTP headers set,
1418
 *   or an error page is shown if now export is available to download.
1419
 */
1420
function webform_results_download_callback($node) {
1421
  if (isset($_SESSION['webform_export_info'])) {
1422
    module_load_include('inc', 'webform', 'includes/webform.export');
1423

    
1424
    $export_info = $_SESSION['webform_export_info'];
1425
    $export_info['exporter'] = webform_export_create_handler($export_info['format'], $export_info['options']);
1426

    
1427
    unset($_SESSION['webform_export_info']);
1428
    if (isset($_COOKIE['webform_export_info'])) {
1429
      unset($_COOKIE['webform_export_info']);
1430
      $params = session_get_cookie_params();
1431
      setcookie('webform_export_info', '', -1, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
1432
    }
1433

    
1434
    webform_results_download($node, $export_info);
1435
  }
1436
  else {
1437
    return t('No export file ready for download. The file may have already been downloaded by your browser. Visit the <a href="!href">download export form</a> to create a new export.', array('!href' => url('node/' . $node->nid . '/webform-results/download')));
1438
  }
1439
}
1440

    
1441
/**
1442
 * Batch API callback; Write the opening byte in the export file.
1443
 */
1444
function webform_results_batch_bof($node, $format = 'delimited', $options = array(), &$context = NULL) {
1445
  module_load_include('inc', 'webform', 'includes/webform.export');
1446

    
1447
  $exporter = webform_export_create_handler($format, $options);
1448
  $handle = fopen($options['file_name'], 'w');
1449
  if (!$handle) {
1450
    return;
1451
  }
1452
  $exporter->bof($handle);
1453
  @fclose($handle);
1454
}
1455

    
1456
/**
1457
 * Batch API callback; Write the headers of the export to the export file.
1458
 */
1459
function webform_results_batch_headers($node, $format = 'delimited', $options = array(), &$context = NULL) {
1460
  module_load_include('inc', 'webform', 'includes/webform.export');
1461

    
1462
  $exporter = webform_export_create_handler($format, $options);
1463
  $handle = fopen($options['file_name'], 'a');
1464
  if (!$handle) {
1465
    return;
1466
  }
1467
  $headers = webform_results_download_headers($node, $options);
1468
  $row_count = 0;
1469
  $col_count = 0;
1470
  foreach ($headers as $row) {
1471
    // Output header if header_keys is non-negative. -1 means no headers.
1472
    if ($options['header_keys'] >= 0) {
1473
      $exporter->add_row($handle, $row, $row_count);
1474
      $row_count++;
1475
    }
1476
    $col_count = count($row) > $col_count ? count($row) : $col_count;
1477
  }
1478
  $context['results']['row_count'] = $row_count;
1479
  $context['results']['col_count'] = $col_count;
1480
  @fclose($handle);
1481
}
1482

    
1483
/**
1484
 * Batch API callback; Write the rows of the export to the export file.
1485
 */
1486
function webform_results_batch_rows($node, $format = 'delimited', $options = array(), &$context = NULL) {
1487
  module_load_include('inc', 'webform', 'includes/webform.export');
1488

    
1489
  // Initialize the sandbox if this is the first execution of the batch
1490
  // operation.
1491
  if (!isset($context['sandbox']['batch_number'])) {
1492
    $context['sandbox']['batch_number'] = 0;
1493
    $context['sandbox']['sid_count'] = webform_download_sids_count($node->nid, $options['range']);
1494
    $context['sandbox']['batch_max'] = max(1, ceil($context['sandbox']['sid_count'] / $options['range']['batch_size']));
1495
    $context['sandbox']['serial'] = 0;
1496
    $context['sandbox']['last_sid'] = 0;
1497
  }
1498

    
1499
  // Retrieve the submissions for this batch process.
1500
  $options['range']['batch_number'] = $context['sandbox']['batch_number'];
1501

    
1502
  $query = webform_download_sids_query($node->nid, $options['range']);
1503

    
1504
  // Join to the users table to include user name in results, as required by
1505
  // webform_results_download_rows_process.
1506
  $query->leftJoin('users', 'u', 'u.uid = ws.uid');
1507
  $query->fields('u', array('name'));
1508
  $query->fields('ws');
1509

    
1510
  if (!empty($options['sids'])) {
1511
    $query->condition('ws.sid', $options['sids'], 'IN');
1512
  }
1513

    
1514
  $submissions = webform_get_submissions_load($query);
1515

    
1516
  $rows = webform_results_download_rows_process($node, $options, $context['sandbox']['serial'], $submissions);
1517

    
1518
  // Write these submissions to the file.
1519
  $exporter = webform_export_create_handler($format, $options);
1520
  $handle = fopen($options['file_name'], 'a');
1521
  if (!$handle) {
1522
    return;
1523
  }
1524
  foreach ($rows as $row) {
1525
    $exporter->add_row($handle, $row, $context['results']['row_count']);
1526
    $context['results']['row_count']++;
1527
  }
1528

    
1529
  $context['sandbox']['serial'] += count($submissions);
1530
  $context['sandbox']['last_sid'] = end($submissions) ? key($submissions) : NULL;
1531
  $context['sandbox']['batch_number']++;
1532

    
1533
  @fclose($handle);
1534

    
1535
  // Display status message.
1536
  $context['message'] = t('Exported @count of @total submissions...', array('@count' => $context['sandbox']['serial'], '@total' => $context['sandbox']['sid_count']));
1537
  $context['finished'] = $context['sandbox']['batch_number'] < $context['sandbox']['batch_max']
1538
                            ? $context['sandbox']['batch_number'] / $context['sandbox']['batch_max']
1539
                            : 1.0;
1540
  $context['results']['last_sid'] = $context['sandbox']['last_sid'];
1541
}
1542

    
1543
/**
1544
 * Batch API callback; Write the closing bytes in the export file.
1545
 */
1546
function webform_results_batch_eof($node, $format = 'delimited', $options = array(), &$context = NULL) {
1547
  module_load_include('inc', 'webform', 'includes/webform.export');
1548

    
1549
  $exporter = webform_export_create_handler($format, $options);
1550

    
1551
  // We open the file for reading and writing, rather than just appending for
1552
  // exporters that need to adjust the beginning of the files as well as the
1553
  // end, i.e. webform_exporter_excel_xlsx::eof().
1554
  $handle = fopen($options['file_name'], 'r+');
1555
  if (!$handle) {
1556
    return;
1557
  }
1558
  // Move pointer to the end of the file.
1559
  fseek($handle, 0, SEEK_END);
1560
  $exporter->eof($handle, $context['results']['row_count'], $context['results']['col_count']);
1561
  @fclose($handle);
1562
}
1563

    
1564
/**
1565
 * Batch API callback; Do any last processing on the finished export.
1566
 */
1567
function webform_results_batch_post_process($node, $format = 'delimited', $options = array(), &$context = NULL) {
1568
  module_load_include('inc', 'webform', 'includes/webform.export');
1569

    
1570
  $context['results']['node'] = $node;
1571
  $context['results']['file_name'] = $options['file_name'];
1572

    
1573
  $exporter = webform_export_create_handler($format, $options);
1574
  $exporter->post_process($context['results']);
1575
}
1576

    
1577
/**
1578
 * Batch API callback; Set the $_SESSION variables used to download the file.
1579
 *
1580
 * Because we want the user to be returned to the main form first, we have to
1581
 * temporarily store information about the created file, send the user to the
1582
 * form, then use JavaScript to request node/x/webform-results/download-file,
1583
 * which will execute webform_results_download_file().
1584
 */
1585
function webform_results_batch_results($node, $format, $options, &$context) {
1586

    
1587
  $export_info = array(
1588
    'format' => $format,
1589
    'options' => $options,
1590
    'file_name' => $context['results']['file_name'],
1591
    'row_count' => $context['results']['row_count'],
1592
    'last_sid' => $context['results']['last_sid'],
1593
  );
1594

    
1595
  if (isset($_SESSION)) {
1596
    // UI exection. Defer resetting last-downloaded sid until download page. Set
1597
    // a session variable containing the information referencing the exported
1598
    // file. A cookie is also set to allow the browser to ensure the redirect to
1599
    // the file only happens one time.
1600
    $_SESSION['webform_export_info'] = $export_info;
1601
    $params = session_get_cookie_params();
1602
    setcookie('webform_export_info', '1', REQUEST_TIME + 120, $params['path'], $params['domain'], $params['secure'], FALSE);
1603
  }
1604
  else {
1605
    // Drush execution of wfx command. Reset last-downloaded sid now. $_SESSION
1606
    // is not supported for command line execution.
1607
    webform_results_export_success($node, $export_info);
1608
  }
1609
}
1610

    
1611
/**
1612
 * Batch API completion callback; Display completion message and cleanup.
1613
 */
1614
function webform_results_batch_finished($success, $results, $operations) {
1615
  if ($success) {
1616
    $download_url = url('node/' . $results['node']->nid . '/webform-results/download-file');
1617
    drupal_set_message(t('Export creation complete. Your download should begin now. If it does not start, <a href="!href">download the file here</a>. This file may only be downloaded once.', array('!href' => $download_url)));
1618
  }
1619
  else {
1620
    drupal_set_message(t('An error occurred while generating the export file.'));
1621
    if (isset($results['file_name']) && is_file($results['file_name'])) {
1622
      @unlink($results['file_name']);
1623
    }
1624
  }
1625
}
1626

    
1627
/**
1628
 * Provides a simple analysis of all submissions to a webform.
1629
 *
1630
 * @param $node
1631
 *   The webform node on which to generate the analysis.
1632
 * @param $sids
1633
 *   An array of submission IDs to which this analysis may be filtered. May be
1634
 *   used to generate results that are per-user or other groups of submissions.
1635
 * @param $analysis_component
1636
 *   A webform component. If passed in, additional information may be returned
1637
 *   relating specifically to that component's analysis, such as a list of
1638
 *   "Other" values within a select list.
1639
 *
1640
 * @return array
1641
 *   Renderable array: A simple analysis of all submissions to a webform.
1642
 */
1643
function webform_results_analysis($node, $sids = array(), $analysis_component = NULL) {
1644
  if (!is_array($sids)) {
1645
    $sids = array();
1646
  }
1647

    
1648
  // Build a renderable for the content of this page.
1649
  $analysis = array(
1650
    '#theme' => array('webform_analysis__' . $node->nid, 'webform_analysis'),
1651
    '#node' => $node,
1652
    '#component' => $analysis_component,
1653
  );
1654

    
1655
  // See if a query (possibly with exposed filter) needs to restrict the
1656
  // submissions that are being analyzed.
1657
  $query = NULL;
1658
  if (empty($sids)) {
1659
    $view = webform_get_view($node, 'webform_analysis');
1660
    if ($view->type != t('Default') || $view->name != 'webform_analysis') {
1661
      // The view has been customized from the no-op built-in view. Use it.
1662
      $view->set_display();
1663
      $view->init_handlers();
1664
      $view->override_url = $_GET['q'];
1665
      $view->preview = TRUE;
1666
      $view->pre_execute(array($node->nid));
1667
      $view->build();
1668
      // Let modules modify the view just prior to executing it.
1669
      foreach (module_implements('views_pre_execute') as $module) {
1670
        $function = $module . '_views_pre_execute';
1671
        $function($view);
1672
      }
1673
      // If the view is already executed, there was an error in generating it.
1674
      $query = $view->executed ? NULL : $view->query->query();
1675
      $view->post_execute();
1676

    
1677
      if (isset($view->exposed_widgets)) {
1678
        $analysis['exposed_filter']['#markup'] = $view->exposed_widgets;
1679
      }
1680
    }
1681
  }
1682

    
1683
  // If showing all components, display selection form.
1684
  if (!$analysis_component) {
1685
    $analysis['form'] = drupal_get_form('webform_analysis_components_form', $node);
1686
  }
1687

    
1688
  // Add all the components to the analysis renderable array.
1689
  $components = isset($analysis_component) ? array($analysis_component['cid']) : webform_analysis_enabled_components($node);
1690
  foreach ($components as $cid) {
1691
    // Do component specific call.
1692
    $component = $node->webform['components'][$cid];
1693
    if ($data = webform_component_invoke($component['type'], 'analysis', $component, $sids, isset($analysis_component), $query)) {
1694
      drupal_alter('webform_analysis_component_data', $data, $node, $component);
1695
      $analysis['data'][$cid] = array(
1696
        '#theme' => array('webform_analysis_component__' . $node->nid . '__' . $cid, 'webform_analysis_component__' . $node->nid, 'webform_analysis_component'),
1697
        '#node' => $node,
1698
        '#component' => $component,
1699
        '#data' => $data,
1700
      );
1701
      $analysis['data'][$cid]['basic'] = array(
1702
        '#theme' => array('webform_analysis_component_basic__' . $node->nid . '__' . $cid, 'webform_analysis_component_basic__' . $node->nid, 'webform_analysis_component_basic'),
1703
        '#component' => $component,
1704
        '#data' => $data,
1705
      );
1706
    }
1707
  }
1708

    
1709
  drupal_alter('webform_analysis', $analysis);
1710
  return $analysis;
1711
}
1712

    
1713
/**
1714
 * Prerender function for webform-analysis.tpl.php.
1715
 */
1716
function template_preprocess_webform_analysis(&$variables) {
1717
  $analysis = $variables['analysis'];
1718
  $variables['node'] = $analysis['#node'];
1719
  $variables['component'] = $analysis['#component'];
1720
}
1721

    
1722
/**
1723
 * Prerender function for webform-analysis-component.tpl.php.
1724
 */
1725
function template_preprocess_webform_analysis_component(&$variables) {
1726
  $component_analysis = $variables['component_analysis'];
1727
  $variables['node'] = $component_analysis['#node'];
1728
  $variables['component'] = $component_analysis['#component'];
1729

    
1730
  // Ensure defaults.
1731
  $variables['component_analysis']['#data'] += array(
1732
    'table_header' => NULL,
1733
    'table_rows' => array(),
1734
    'other_data' => array(),
1735
  );
1736
  $variables['classes_array'][] = 'webform-analysis-component-' . $variables['component']['type'];
1737
  $variables['classes_array'][] = 'webform-analysis-component--' . str_replace('_', '-', implode('--', webform_component_parent_keys($variables['node'], $variables['component'])));
1738
}
1739

    
1740
/**
1741
 * Render an individual component's analysis data in a table.
1742
 *
1743
 * @param $variables
1744
 *   An array of theming variables for this theme function. Included keys:
1745
 *   - $component: The component whose analysis is being rendered.
1746
 *   - $data: An array of array containing the analysis data. Contains the keys:
1747
 *     - table_header: If this table has more than a single column, an array
1748
 *       of header labels.
1749
 *     - table_rows: If this component has a table that should be rendered, an
1750
 *       array of values
1751
 *
1752
 * @return string
1753
 *   The rendered table.
1754
 */
1755
function theme_webform_analysis_component_basic($variables) {
1756
  $data = $variables['data'];
1757

    
1758
  // Ensure defaults.
1759
  $data += array(
1760
    'table_header' => NULL,
1761
    'table_rows' => array(),
1762
    'other_data' => array(),
1763
  );
1764

    
1765
  // Combine the "other data" into the table rows by default.
1766
  if (is_array($data['other_data'])) {
1767
    foreach ($data['other_data'] as $other_data) {
1768
      if (is_array($other_data)) {
1769
        $data['table_rows'][] = $other_data;
1770
      }
1771
      else {
1772
        $data['table_rows'][] = array(
1773
          array(
1774
            'colspan' => 2,
1775
            'data' => $other_data,
1776
          ),
1777
        );
1778
      }
1779
    }
1780
  }
1781
  elseif (strlen($data['other_data'])) {
1782
    $data['table_rows'][] = array(
1783
      'colspan' => 2,
1784
      'data' => $data['other_data'],
1785
    );
1786
  }
1787

    
1788
  return theme('table', array(
1789
    'header' => $data['table_header'],
1790
    'rows' => $data['table_rows'],
1791
    'sticky' => FALSE,
1792
    'attributes' => array('class' => array('webform-analysis-table')),
1793
  ));
1794
}
1795

    
1796
/**
1797
 * Return a list of components that should be displayed for analysis.
1798
 *
1799
 * @param $node
1800
 *   The node whose components' data is being analyzed.
1801
 *
1802
 * @return array
1803
 *   An array of component IDs.
1804
 */
1805
function webform_analysis_enabled_components($node) {
1806
  $cids = array();
1807
  foreach ($node->webform['components'] as $cid => $component) {
1808
    if (!empty($component['extra']['analysis'])) {
1809
      $cids[] = $cid;
1810
    }
1811
  }
1812
  return $cids;
1813
}
1814

    
1815
/**
1816
 * Form for selecting which components should be shown on the analysis page.
1817
 */
1818
function webform_analysis_components_form($form, &$form_state, $node) {
1819
  form_load_include($form_state, 'inc', 'webform', 'includes/webform.components');
1820
  $form['#node'] = $node;
1821

    
1822
  $component_list = webform_component_list($node, 'analysis', TRUE);
1823
  $enabled_components = webform_analysis_enabled_components($node);
1824
  if (empty($component_list)) {
1825
    $help = t('No components have added that support analysis. <a href="!url">Add components to your form</a> to see calculated data.', array('!url' => url('node/' . $node->nid . '/webform')));
1826
  }
1827
  elseif (empty($enabled_components)) {
1828
    $help = t('No components have analysis enabled in this form. Enable analysis under the "Add analysis components" fieldset.');
1829
  }
1830
  else {
1831
    $help = t('This page shows analysis of submitted data, such as the number of submissions per component value, calculations, and averages. Additional components may be added under the "Add analysis components" fieldset.');
1832
  }
1833

    
1834
  $form['help'] = array(
1835
    '#markup' => '<p>' . $help . '</p>',
1836
    '#access' => !empty($help),
1837
    '#weight' => -100,
1838
  );
1839

    
1840
  $form['components'] = array(
1841
    '#type' => 'select',
1842
    '#title' => t('Add analysis components'),
1843
    '#options' => $component_list,
1844
    '#default_value' => $enabled_components,
1845
    '#multiple' => TRUE,
1846
    '#size' => 10,
1847
    '#description' => t('The selected components will be included on the analysis page.'),
1848
    '#process' => array('webform_component_select'),
1849
    '#access' => count($component_list),
1850
  );
1851

    
1852
  $form['actions'] = array(
1853
    '#type' => 'actions',
1854
    '#access' => count($component_list),
1855
  );
1856
  $form['actions']['submit'] = array(
1857
    '#type' => 'submit',
1858
    '#value' => t('Update analysis display'),
1859
  );
1860

    
1861
  return $form;
1862
}
1863

    
1864
/**
1865
 * Submit handler for webform_analysis_components_form().
1866
 */
1867
function webform_analysis_components_form_submit($form, $form_state) {
1868
  $node = $form['#node'];
1869
  // Get a fresh copy of the node so that we are only saving these changes.
1870
  // Otherwise, changes to the Webform on another tab will be overwritten.
1871
  $node = node_load($node->nid, NULL, TRUE);
1872
  foreach ($form_state['values']['components'] as $cid => $enabled) {
1873
    $node->webform['components'][$cid]['extra']['analysis'] = (bool) $enabled;
1874
  }
1875
  node_save($node);
1876
}
1877

    
1878
/**
1879
 * Output the content of the Analysis page.
1880
 *
1881
 * @see webform_results_analysis()
1882
 */
1883
function theme_webform_results_analysis($variables) {
1884
  $node = $variables['node'];
1885
  $data = $variables['data'];
1886
  $analysis_component = $variables['component'];
1887

    
1888
  $rows = array();
1889
  $question_number = 0;
1890
  $single = isset($analysis_component);
1891

    
1892
  $header = array(
1893
    $single ? $analysis_component['name'] : t('Q'),
1894
    array('data' => $single ? '&nbsp;' : t('responses'), 'colspan' => '10'),
1895
  );
1896

    
1897
  foreach ($data as $cid => $row_data) {
1898
    $question_number++;
1899

    
1900
    if (is_array($row_data)) {
1901
      $row = array();
1902
      if (!$single) {
1903
        $row['data'][] = array('data' => '<strong>' . $question_number . '</strong>', 'rowspan' => count($row_data) + 1, 'valign' => 'top');
1904
        $row['data'][] = array('data' => '<strong>' . check_plain($node->webform['components'][$cid]['name']) . '</strong>', 'colspan' => '10');
1905
        $row['class'][] = 'webform-results-question';
1906
      }
1907
      $rows = array_merge($rows, array_merge(array($row), $row_data));
1908
    }
1909
  }
1910

    
1911
  if (count($rows) == 0) {
1912
    $rows[] = array(array('data' => t('There are no submissions for this form. <a href="!url">View this form</a>.', array('!url' => url('node/' . $node->nid))), 'colspan' => 20));
1913
  }
1914

    
1915
  return theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE, 'attributes' => array('class' => array('webform-results-analysis'))));
1916
}
1917

    
1918
/**
1919
 * Given a set of range options, retrieve a set of SIDs for a webform node.
1920
 *
1921
 * @param int $nid
1922
 *   The node id of the webform.
1923
 * @param array $range_options
1924
 *   Associate array of range options.
1925
 * @param int $uid
1926
 *   The user id of the user whose last download information should be used,
1927
 *   or the current user if NULL. This is unrelated to which user submitted
1928
 *   the submissions.
1929
 *
1930
 * @return array
1931
 *   An array of submission IDs.
1932
 */
1933
function webform_download_sids($nid, array $range_options, $uid = NULL) {
1934
  return webform_download_sids_query($nid, $range_options, $uid)
1935
    ->fields('ws', array('sid'))
1936
    ->execute()
1937
    ->fetchCol();
1938
}
1939

    
1940
/**
1941
 * Retrieves a count the number of matching submissions.
1942
 *
1943
 * @return int
1944
 *   The number number of matching submissions.
1945
 */
1946
function webform_download_sids_count($nid, $range_options, $uid = NULL) {
1947
  return (int) webform_download_sids_query($nid, $range_options, $uid)
1948
    ->countQuery()
1949
    ->execute()
1950
    ->fetchField();
1951
}
1952

    
1953
/**
1954
 * Given a set of range options, return an unexecuted select query.
1955
 *
1956
 * The query will have no fields as they should be added by the caller as
1957
 * desired.
1958
 *
1959
 * @param int $nid
1960
 *   The node id of the webform.
1961
 * @param array $range_options
1962
 *   Associate array of range options.
1963
 * @param int $uid
1964
 *   The user id of the user whose last download information should be used,
1965
 *   or the current user if NULL. This is unrelated to which user submitted
1966
 *   the submissions.
1967
 *
1968
 * @return QueryAlterableInterface
1969
 *   The query object.
1970
 */
1971
function webform_download_sids_query($nid, array $range_options, $uid = NULL) {
1972
  $query = db_select('webform_submissions', 'ws')
1973
    ->condition('ws.nid', $nid)
1974
    ->addTag('webform_download_sids');
1975

    
1976
  switch ($range_options['range_type']) {
1977
    case 'all':
1978
      // All Submissions.
1979
      $query->orderBy('ws.sid', 'ASC');
1980
      break;
1981

    
1982
    case 'latest':
1983
      // Last x Submissions.
1984
      $start_sid = webform_download_latest_start_sid($nid, $range_options['latest'], $range_options['completion_type']);
1985
      $query->condition('ws.sid', $start_sid, '>=');
1986
      break;
1987

    
1988
    case 'range':
1989
      // Submissions Start-End.
1990
      $query->condition('ws.sid', $range_options['start'], '>=');
1991
      if ($range_options['end']) {
1992
        $query->condition('ws.sid', $range_options['end'], '<=');
1993
      }
1994
      $query->orderBy('ws.sid', 'ASC');
1995
      break;
1996

    
1997
    case 'range_serial':
1998
      // Submissions Start-End, using serial numbers.
1999
      $query->condition('ws.serial', $range_options['start'], '>=');
2000
      if ($range_options['end']) {
2001
        $query->condition('ws.serial', $range_options['end'], '<=');
2002
      }
2003
      $query->orderBy('ws.serial', 'ASC');
2004
      break;
2005

    
2006
    case 'new':
2007
      // All since last download. This is the same as 'range_date' except that
2008
      // the start date is the date of the last download.
2009
      $download_info = webform_download_last_download_info($nid, $uid);
2010
      $start_date = (int) $download_info ? $download_info['requested'] : 0;
2011
      $end_time = NULL;
2012
      // Fall through.
2013
    case 'range_date':
2014
      // If the submission is completed, use the completed date, otherwise, the
2015
      // submitted date.
2016
      $date_field = 'COALESCE(NULLIF(ws.completed, 0), ws.submitted)';
2017
      // This is required because SQLite uses dynamic typing.
2018
      if (db_driver() === 'sqlite') {
2019
        $date_field = 'CAST(' . $date_field . ' AS INTEGER)';
2020
      }
2021

    
2022
      if (!isset($start_date)) {
2023
        $format = webform_date_format('short');
2024
        $start_date = DateTime::createFromFormat($format, $range_options['start_date']);
2025
        $start_date->setTime(0, 0, 0);
2026
        $start_date = $start_date->getTimestamp();
2027

    
2028
        $end_time = DateTime::createFromFormat($format, $range_options['end_date']);
2029
      }
2030
      $query->where($date_field . ' >= :start_date', array(':start_date' => $start_date));
2031

    
2032
      if ($end_time) {
2033
        // Check for the full day's submissions.
2034
        $end_time->setTime(23, 59, 59);
2035
        $end_time = $end_time->getTimestamp();
2036
        $query->where($date_field . ' <= :end_time', array(':end_time' => $end_time));
2037
      }
2038

    
2039
      $query->orderBy($date_field, 'ASC');
2040
      break;
2041
  }
2042

    
2043
  // Filter down to draft or finished submissions.
2044
  if (!empty($range_options['completion_type']) && $range_options['completion_type'] !== 'all') {
2045
    $query->condition('is_draft', (int) ($range_options['completion_type'] === 'draft'));
2046
  }
2047

    
2048
  if (isset($range_options['batch_number']) && !empty($range_options['batch_size'])) {
2049
    $query->range($range_options['batch_number'] * $range_options['batch_size'], $range_options['batch_size']);
2050
  }
2051

    
2052
  drupal_alter('webform_download_sids_query', $query);
2053

    
2054
  return $query;
2055
}
2056

    
2057
/**
2058
 * Get this user's last download information, including the SID and timestamp.
2059
 *
2060
 * This function provides an array of information about the last download that
2061
 * a user had for a particular Webform node. Currently it only returns an array
2062
 * with two keys:
2063
 *  - sid: The last submission ID that was downloaded.
2064
 *  - requested: The timestamp of the last download request.
2065
 *
2066
 * @param $nid
2067
 *   The Webform NID.
2068
 * @param $uid
2069
 *   The user account ID for which to retrieve download information.
2070
 *
2071
 * @return array|false
2072
 *   An array of download information or FALSE if this user has never downloaded
2073
 *   results for this particular node.
2074
 */
2075
function webform_download_last_download_info($nid, $uid = NULL) {
2076
  $uid = isset($uid) ? $uid : $GLOBALS['user']->uid;
2077

    
2078
  $query = db_select('webform_last_download', 'wld');
2079
  $query->leftJoin('webform_submissions', 'wfs', 'wld.sid = wfs.sid');
2080
  $info = $query
2081
    ->fields('wld')
2082
    ->fields('wfs', array('serial'))
2083
    ->condition('wld.nid', $nid)
2084
    ->condition('wld.uid', $uid)
2085
    ->execute()
2086
    ->fetchAssoc();
2087

    
2088
  return $info;
2089
}
2090

    
2091
/**
2092
 * Get an SID based a requested latest count.
2093
 *
2094
 * @param int $nid
2095
 *   The webform NID.
2096
 * @param int $latest_count
2097
 *   The latest count on which the SID will be retrieved.
2098
 * @param string $completion_type
2099
 *   The completion type, either "finished", "draft", or "all".
2100
 *
2101
 * @return int
2102
 *   The submission ID that starts the latest sequence of submissions.
2103
 */
2104
function webform_download_latest_start_sid($nid, $latest_count, $completion_type = 'all') {
2105
  // @todo: Find a more efficient DBTNG query to retrieve this number.
2106
  $query = db_select('webform_submissions', 'ws')
2107
    ->fields('ws', array('sid'))
2108
    ->condition('nid', $nid)
2109
    ->orderBy('ws.sid', 'DESC')
2110
    ->range(0, $latest_count)
2111
    ->addTag('webform_download_latest_start_sid');
2112

    
2113
  if ($completion_type !== 'all') {
2114
    $query->condition('is_draft', (int) ($completion_type === 'draft'));
2115
  }
2116

    
2117
  $latest_sids = $query->execute()->fetchCol();
2118
  return $latest_sids ? min($latest_sids) : 1;
2119
}