summaryrefslogtreecommitdiff
path: root/sites/all/modules/views_bulk_operations/actions/archive.action.inc
blob: f00552743d6fe890daaa18dce908972d948e6b12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
<?php

/**
 * @file
 * Provides an action for creating a zip archive of selected files.
 * An entry in the {file_managed} table is created for the newly created archive,
 * and it is marked as permanent or temporary based on the operation settings.
 */

function views_bulk_operations_archive_action_info() {
  $actions = array();
  if (function_exists('zip_open')) {
    $actions['views_bulk_operations_archive_action'] = array(
      'type' => 'file',
      'label' => t('Create an archive of selected files'),
      // This action only works when invoked through VBO. That's why it's
      // declared as non-configurable to prevent it from being shown in the
      // "Create an advanced action" dropdown on admin/config/system/actions.
      'configurable' => FALSE,
      'vbo_configurable' => TRUE,
      'behavior' => array('views_property'),
      'triggers' => array('any'),
    );
  }
  return $actions;
}

/**
 * Since Drupal's Archiver doesn't abstract properly the archivers it implements
 * (Archive_Tar and ZipArchive), it can't be used here.
 */
function views_bulk_operations_archive_action($file, $context) {
  global $user;
  static $archive_contents = array();

  // Adding a non-existent file to the archive crashes ZipArchive on close().
  if (file_exists($file->uri)) {
    $destination = $context['destination'];
    $zip = new ZipArchive();
    // If the archive already exists, open it. If not, create it.
    if (file_exists($destination)) {
      $opened = $zip->open(drupal_realpath($destination));
    }
    else {
      $opened = $zip->open(drupal_realpath($destination), ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
    }

    if ($opened) {
      // Create a list of all files in the archive. Used for duplicate checking.
      if (empty($archive_contents)) {
        for ($i = 0; $i < $zip->numFiles; $i++) {
          $archive_contents[] = $zip->getNameIndex($i);
        }
      }
      // Make sure that the target filename is unique.
      $filename = _views_bulk_operations_archive_action_create_filename(basename($file->uri), $archive_contents);
      // Note that the actual addition happens on close(), hence the need
      // to open / close the archive each time the action runs.
      $zip->addFile(drupal_realpath($file->uri), $filename);
      $zip->close();
      $archive_contents[] = $filename;
    }
  }

  // The operation is complete, create a file entity and provide a download
  // link to the user.
  if ($context['progress']['current'] == $context['progress']['total']) {
    $archive_file = new stdClass();
    $archive_file->uri      = $destination;
    $archive_file->filename = basename($destination);
    $archive_file->filemime = file_get_mimetype($destination);
    $archive_file->uid      = $user->uid;
    $archive_file->status   = $context['settings']['temporary'] ? FALSE : FILE_STATUS_PERMANENT;
    file_save($archive_file);

    $url = file_create_url($archive_file->uri);
    $url = l($url, $url, array('absolute' => TRUE));
    _views_bulk_operations_log(t('An archive has been created and can be downloaded from: !url', array('!url' => $url)));
  }
}

/**
 * Configuration form shown to the user before the action gets executed.
 */
function views_bulk_operations_archive_action_form($context) {
  // Pass the scheme as a value, so that the submit callback can access it.
  $form['scheme'] = array(
    '#type' => 'value',
    '#value' => $context['settings']['scheme'],
  );

  $form['filename'] = array(
    '#type' => 'textfield',
    '#title' => t('Filename'),
    '#default_value' => 'vbo_archive_' . date('Ymd'),
    '#field_suffix' => '.zip',
    '#description' => t('The name of the archive file.'),
  );
  return $form;
}

/**
 * Assembles a sanitized and unique URI for the archive, and returns it for
 * usage by the action callback (views_bulk_operations_archive_action).
 */
function views_bulk_operations_archive_action_submit($form, $form_state) {
  // Validate the scheme, fallback to public if it's somehow invalid.
  $scheme = $form_state['values']['scheme'];
  if (!file_stream_wrapper_valid_scheme($scheme)) {
    $scheme = 'public';
  }
  $destination = $scheme . '://' . basename($form_state['values']['filename']) . '.zip';
  // If the chosen filename already exists, file_destination() will append
  // an integer to it in order to make it unique.
  $destination = file_destination($destination, FILE_EXISTS_RENAME);

  return array(
    'destination' => $destination,
  );
}

/**
 * Settings form (embedded into the VBO field settings in the Views UI).
 */
function views_bulk_operations_archive_action_views_bulk_operations_form($options) {
  $scheme_options = array();
  foreach (file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL_NORMAL) as $scheme => $stream_wrapper) {
    $scheme_options[$scheme] = $stream_wrapper['name'];
  }
  if (count($scheme_options) > 1) {
    $form['scheme'] = array(
      '#type' => 'radios',
      '#title' => t('Storage'),
      '#options' => $scheme_options,
      '#default_value' => !empty($options['scheme']) ? $options['scheme'] : variable_get('file_default_scheme', 'public'),
      '#description' => t('Select where the archive should be stored. Private file storage has significantly more overhead than public files, but allows restricted access.'),
    );
  }
  else {
    $scheme_option_keys = array_keys($scheme_options);
    $form['scheme'] = array(
      '#type' => 'value',
      '#value' => reset($scheme_option_keys),
    );
  }

  $form['temporary'] = array(
    '#type' => 'checkbox',
    '#title' => t('Temporary'),
    '#default_value' => isset($options['temporary']) ? $options['temporary'] : TRUE,
    '#description' => t('Temporary files older than 6 hours are removed when cron runs.'),
  );
  return $form;
}

/**
 * Create a sanitized and unique version of the provided filename.
 *
 * @param $filename
 *   String filename
 *
 * @return
 *   The new filename.
 */
function _views_bulk_operations_archive_action_create_filename($filename, $archive_list) {
  // Strip control characters (ASCII value < 32). Though these are allowed in
  // some filesystems, not many applications handle them well.
  $filename = preg_replace('/[\x00-\x1F]/u', '_', $filename);
  if (substr(PHP_OS, 0, 3) == 'WIN') {
    // These characters are not allowed in Windows filenames
    $filename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $filename);
  }

  if (in_array($filename, $archive_list)) {
    // Destination file already exists, generate an alternative.
    $pos = strrpos($filename, '.');
    if ($pos !== FALSE) {
      $name = substr($filename, 0, $pos);
      $ext = substr($filename, $pos);
    }
    else {
      $name = $filename;
      $ext = '';
    }

    $counter = 0;
    do {
      $filename = $name . '_' . $counter++ . $ext;
    } while (in_array($filename, $archive_list));
  }

  return $filename;
}