Batch.php 13.4 KB
Newer Older
Pawel G's avatar
Pawel G committed
1
2
3
4
<?php
/**
 * @file
 * Contains \Drupal\simple_sitemap\Batch.
5
6
7
 *
 * Helper functions for the Drupal batch API.
 * @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
Pawel G's avatar
Pawel G committed
8
9
10
11
12
13
14
 */

namespace Drupal\simple_sitemap;

use Drupal\user\Entity\User;
use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
15
use Drupal\Core\Cache\Cache;
Pawel G's avatar
Pawel G committed
16
17
18

class Batch {
  private $batch;
19
  private $batchInfo;
Pawel G's avatar
Pawel G committed
20
21
22

  const PATH_DOES_NOT_EXIST = "The path @faulty_path has been omitted from the XML sitemap, as it does not exist.";
  const PATH_DOES_NOT_EXIST_OR_NO_ACCESS = "The path @faulty_path has been omitted from the XML sitemap as it either does not exist, or it is not accessible to anonymous users.";
23
24
25
  const BATCH_INIT_MESSAGE = 'Initializing batch...';
  const BATCH_ERROR_MESSAGE = 'An error has occurred. This may result in an incomplete XML sitemap.';
  const BATCH_PROGRESS_MESSAGE = 'Processing @current out of @total link types.';
26
  const ANONYMOUS_USER_ID = 0;
Pawel G's avatar
Pawel G committed
27

28
  function __construct() {
Pawel G's avatar
Pawel G committed
29
    $this->batch = [
Pawel G's avatar
Pawel G committed
30
      'title' => t('Generating XML sitemap'),
31
32
33
      'init_message' => t(self::BATCH_INIT_MESSAGE),
      'error_message' => t(self::BATCH_ERROR_MESSAGE),
      'progress_message' => t(self::BATCH_PROGRESS_MESSAGE),
Pawel G's avatar
Pawel G committed
34
35
36
      'operations' => [],
      'finished' => [__CLASS__, 'finishGeneration'], // __CLASS__ . '::finishGeneration' not working possibly due to a drush error.
    ];
37
38
39
40
  }

  public function setBatchInfo($batch_info) {
    $this->batchInfo = $batch_info;
Pawel G's avatar
Pawel G committed
41
42
  }

43
44
45
  /**
   * Starts the batch process depending on where it was requested from.
   */
Pawel G's avatar
Pawel G committed
46
  public function start() {
47
    switch ($this->batchInfo['from']) {
48

Pawel G's avatar
Pawel G committed
49
      case 'form':
50
        batch_set($this->batch);
Pawel G's avatar
Pawel G committed
51
        break;
52

Pawel G's avatar
Pawel G committed
53
      case 'drush':
54
        batch_set($this->batch);
Pawel G's avatar
Pawel G committed
55
        $this->batch =& batch_get();
Pawel G's avatar
Pawel G committed
56
        $this->batch['progressive'] = FALSE;
57
        drush_log(t(self::BATCH_INIT_MESSAGE), 'status');
Pawel G's avatar
Pawel G committed
58
59
        drush_backend_batch_process();
        break;
60

61
      case 'backend':
62
        batch_set($this->batch);
Pawel G's avatar
Pawel G committed
63
        $this->batch =& batch_get();
64
        $this->batch['progressive'] = FALSE;
Pawel G's avatar
Pawel G committed
65
        batch_process(); //todo: Does not take advantage of batch API and eventually runs out of memory on very large sites.
66
        break;
67
68

      case 'nobatch':
Pawel G's avatar
Pawel G committed
69
        $context = [];
70
71
72
73
        foreach($this->batch['operations'] as $i => $operation) {
          $operation[1][] = &$context;
          call_user_func_array($operation[0], $operation[1]);
        }
Pawel G's avatar
Pawel G committed
74
        self::finishGeneration(TRUE, $context['results'], []);
75
        break;
Pawel G's avatar
Pawel G committed
76
77
78
    }
  }

79
  /**
Pawel G's avatar
Pawel G committed
80
   * Adds an operation to the batch.
81
   *
Pawel G's avatar
Pawel G committed
82
83
   * @param string $processing_method
   * @param array $data
84
   */
Pawel G's avatar
Pawel G committed
85
86
87
88
  public function addOperation($processing_method, $data) {
    $this->batch['operations'][] = [
      __CLASS__ . '::' . $processing_method, [$data, $this->batchInfo]
    ];
Pawel G's avatar
Pawel G committed
89
90
  }

91
92
93
94
95
  /**
   * Callback function called by the batch API when all operations are finished.
   *
   * @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
   */
96
  public static function finishGeneration($success, $results, $operations) {
Pawel G's avatar
Pawel G committed
97
    if ($success) {
98
      $remove_sitemap = empty($results['chunk_count']);
99
      if (!empty($results['generate']) || $remove_sitemap) {
100
        SitemapGenerator::generateSitemap($results['generate'], $remove_sitemap);
Pawel G's avatar
Pawel G committed
101
      }
Pawel G's avatar
Pawel G committed
102
      Cache::invalidateTags(['simple_sitemap']);
103
      drupal_set_message(t("The <a href='@url' target='_blank'>XML sitemap</a> has been regenerated for all languages.",
Pawel G's avatar
Pawel G committed
104
        ['@url' => $GLOBALS['base_url'] . '/sitemap.xml']));
Pawel G's avatar
Pawel G committed
105
106
    }
    else {
107
      //todo: register error
Pawel G's avatar
Pawel G committed
108
109
110
    }
  }

111
112
113
114
  private static function isBatch($batch_info) {
    return $batch_info['from'] != 'nobatch';
  }

115
116
  private static function needsInitialization($context) {
    return empty($context['sandbox']);
117
118
  }

Pawel G's avatar
Pawel G committed
119
  /**
120
   * Batch callback function which generates urls to entity paths.
Pawel G's avatar
Pawel G committed
121
   *
122
   * @param array $entity_info
123
124
   * @param array $batch_info
   * @param array &$context
Pawel G's avatar
Pawel G committed
125
   *
126
   * @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
Pawel G's avatar
Pawel G committed
127
   */
128
129
  public static function generateBundleUrls($entity_info, $batch_info, &$context) {
    $query = \Drupal::entityQuery($entity_info['entity_type_name']);
Pawel G's avatar
Pawel G committed
130
    if (!empty($entity_info['keys']['id']))
131
      $query->sort($entity_info['keys']['id'], 'ASC');
Pawel G's avatar
Pawel G committed
132
    if (!empty($entity_info['keys']['bundle']))
133
      $query->condition($entity_info['keys']['bundle'], $entity_info['bundle_name']);
Pawel G's avatar
Pawel G committed
134
    if (!empty($entity_info['keys']['status']))
135
      $query->condition($entity_info['keys']['status'], 1);
Pawel G's avatar
Pawel G committed
136

137
    // Initialize batch if not done yet.
138
139
140
    if (self::needsInitialization($context)) {
      $count_query = clone $query;
      self::InitializeBatch($batch_info, $count_query->count()->execute(), $context);
141
142
    }

Pawel G's avatar
Pawel G committed
143
    // Creating a query limited to n=batch_process_limit entries.
144
    if (self::isBatch($batch_info)) {
145
      $query->range($context['sandbox']['progress'], $batch_info['batch_process_limit']);
146
    }
Pawel G's avatar
Pawel G committed
147

148
149
    $results = $query->execute();
    if (!empty($results)) {
Pawel G's avatar
Pawel G committed
150
151
      $languages = \Drupal::languageManager()->getLanguages();
      $anon_user = User::load(self::ANONYMOUS_USER_ID);
152
      $entities = \Drupal::entityTypeManager()->getStorage($entity_info['entity_type_name'])->loadMultiple($results);
Pawel G's avatar
Pawel G committed
153

154
155
156
      foreach ($entities as $entity_id => $entity) {
        if (self::isBatch($batch_info)) {
          self::SetCurrentId($entity_id, $context); //todo: move outside of this loop
157
158
        }

159
        // Overriding entity settings if it has been overridden on entity edit page...
160
        if (isset($batch_info['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['index'])) {
Pawel G's avatar
Pawel G committed
161

162
          // Skipping entity if it has been excluded on entity edit page.
163
          if (!$batch_info['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['index']) {
164
165
166
            continue;
          }
          // Otherwise overriding priority settings for this entity.
167
          $priority = $batch_info['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['priority'];
Pawel G's avatar
Pawel G committed
168
        }
169

170
171
172
173
174
175
176
        switch ($entity_info['entity_type_name']) {
          case 'menu_link_content': // Loading url object for menu links.
            if (!$entity->isEnabled())
              continue;
            $url_object = $entity->getUrlObject();
            break;
          default: // Loading url object for other entities.
Pawel G's avatar
Pawel G committed
177
            $url_object = $entity->toUrl();
Pawel G's avatar
Pawel G committed
178
        }
179

180
        // Do not include path if anonymous users do not have access to it.
Pawel G's avatar
Pawel G committed
181
        if (!$url_object->access($anon_user))
182
183
184
185
186
187
188
          continue;

        // Do not include path if it already exists.
        $path = $url_object->getInternalPath();
        if ($batch_info['remove_duplicates'] && self::pathProcessed($path, $context))
          continue;

Pawel G's avatar
Pawel G committed
189
        $urls = [];
190
        foreach ($languages as $language) {
191
192
          if ($language->isDefault()) {
            $urls[$language->getId()] = $url_object->toString();
193
194
          }
          else {
195
//            if ($entity->hasTranslation($language->getId())) {
196
197
            $url_object->setOption('language', $language);
            $urls[$language->getId()] = $url_object->toString();
198
//            }
199
200
201
          }
        }

Pawel G's avatar
Pawel G committed
202
        $context['results']['generate'][] = [
203
204
          'path' => $path,
          'urls' => $urls,
205
206
          'entity_info' => ['entity_type' => $entity_info['entity_type_name'], 'id' => $entity_id],
//          'options' => $url_object->getOptions(), // Not needed, removed to save resources
207
208
          'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
          'priority' => isset($priority) ? $priority : (isset($entity_info['bundle_settings']['priority']) ? $entity_info['bundle_settings']['priority'] : NULL),
Pawel G's avatar
Pawel G committed
209
        ];
210
211
        $priority = NULL;
      }
Pawel G's avatar
Pawel G committed
212
    }
213

214
    if (self::isBatch($batch_info)) {
Pawel G's avatar
Pawel G committed
215
      self::setProgressInfo($context);
216
    }
217
    self::processSegment($context, $batch_info);
Pawel G's avatar
Pawel G committed
218
219
  }

Pawel G's avatar
Pawel G committed
220
  /**
221
   * Batch function which generates urls to custom paths.
Pawel G's avatar
Pawel G committed
222
   *
223
224
225
   * @param array $custom_paths
   * @param array $batch_info
   * @param array &$context
Pawel G's avatar
Pawel G committed
226
   *
227
   * @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
Pawel G's avatar
Pawel G committed
228
   */
229
  public static function generateCustomUrls($custom_paths, $batch_info, &$context) {
Pawel G's avatar
Pawel G committed
230
231

    $languages = \Drupal::languageManager()->getLanguages();
Pawel G's avatar
Pawel G committed
232
    $anon_user = User::load(self::ANONYMOUS_USER_ID);
Pawel G's avatar
Pawel G committed
233

234
    // Initialize batch if not done yet.
235
236
    if (self::needsInitialization($context)) {
      self::InitializeBatch($batch_info, count($custom_paths), $context);
237
    }
Pawel G's avatar
Pawel G committed
238
239

    foreach($custom_paths as $i => $custom_path) {
240
241
242
      if (self::isBatch($batch_info)) {
        self::SetCurrentId($i, $context);
      }
243
      if (!\Drupal::service('path.validator')->isValid($custom_path['path'])) { //todo: Change to different function, as this also checks if current user has access. The user however varies depending if process was started from the web interface or via cron/drush.
Pawel G's avatar
Pawel G committed
244
        self::registerError(self::PATH_DOES_NOT_EXIST_OR_NO_ACCESS, ['@faulty_path' => $custom_path['path']], 'warning');
Pawel G's avatar
Pawel G committed
245
246
        continue;
      }
247
      $options = ['absolute' => TRUE, 'language' => $languages[Simplesitemap::getDefaultLangId()]];
Pawel G's avatar
Pawel G committed
248
      $url_object = Url::fromUserInput($custom_path['path'], $options);
Pawel G's avatar
Pawel G committed
249

Pawel G's avatar
Pawel G committed
250
      if (!$url_object->access($anon_user))
Pawel G's avatar
Pawel G committed
251
252
253
        continue;

      $path = $url_object->getInternalPath();
254
      if ($batch_info['remove_duplicates'] && self::pathProcessed($path, $context))
255
        continue;
Pawel G's avatar
Pawel G committed
256

Pawel G's avatar
Pawel G committed
257
      $urls = [];
258
259
260
      foreach ($languages as $language) {
        if ($language->isDefault()) {
          $urls[$language->getId()] = $url_object->toString();
Pawel G's avatar
Pawel G committed
261
262
        }
        else {
263
264
          $url_object->setOption('language', $language);
          $urls[$language->getId()] = $url_object->toString();
Pawel G's avatar
Pawel G committed
265
266
        }
      }
267

Pawel G's avatar
Pawel G committed
268
      $context['results']['generate'][] = [
Pawel G's avatar
Pawel G committed
269
270
        'path' => $path,
        'urls' => $urls,
271
//        'options' => $url_object->getOptions(), // Not needed, removed to save resources
272
        'priority' => isset($custom_path['priority']) ? $custom_path['priority'] : NULL,
Pawel G's avatar
Pawel G committed
273
      ];
Pawel G's avatar
Pawel G committed
274
    }
275
    if (self::isBatch($batch_info)) {
Pawel G's avatar
Pawel G committed
276
      self::setProgressInfo($context);
277
    }
278
    self::processSegment($context, $batch_info);
279
280
  }

281
  private static function pathProcessed($path, &$context) {
Pawel G's avatar
Pawel G committed
282
    $path_pool = isset($context['results']['processed_paths']) ? $context['results']['processed_paths'] : [];
283
    if (in_array($path, $path_pool)) {
284
285
      return TRUE;
    }
286
    $context['results']['processed_paths'][] = $path;
287
288
289
    return FALSE;
  }

290
  private static function InitializeBatch($batch_info, $max, &$context) {
Pawel G's avatar
Pawel G committed
291
    $context['results']['generate'] = !empty($context['results']['generate']) ? $context['results']['generate'] : [];
292
293
294
295
    if (self::isBatch($batch_info)) {
      $context['sandbox']['progress'] = 0;
      $context['sandbox']['current_id'] = 0;
      $context['sandbox']['max'] = $max;
Pawel G's avatar
Pawel G committed
296
      $context['results']['processed_paths'] = !empty($context['results']['processed_paths']) ? $context['results']['processed_paths'] : [];
297
298
299
    }
  }

300
  private static function SetCurrentId($id, &$context) {
301
302
    $context['sandbox']['progress']++;
    $context['sandbox']['current_id'] = $id;
303
    $context['results']['link_count'] = !isset($context['results']['link_count']) ? 1 : $context['results']['link_count'] + 1; //Not used ATM.
304
305
  }

Pawel G's avatar
Pawel G committed
306
  private static function setProgressInfo(&$context) {
307
308
309
310
311
312
313
    if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
      // Providing progress info to the batch API.
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
      // Adding processing message after finishing every batch segment.
      end($context['results']['generate']);
      $last_key = key($context['results']['generate']);
      if (!empty($context['results']['generate'][$last_key]['path'])) {
Pawel G's avatar
Pawel G committed
314
        $context['message'] = t("Processing path @current out of @max: @path", [
315
316
317
          '@current' => $context['sandbox']['progress'],
          '@max' => $context['sandbox']['max'],
          '@path' => HTML::escape($context['results']['generate'][$last_key]['path']),
Pawel G's avatar
Pawel G committed
318
        ]);
319
320
321
322
      }
    }
  }

323
  private static function processSegment(&$context, $batch_info) {
324
325
    if (!empty($batch_info['max_links']) && count($context['results']['generate']) >= $batch_info['max_links']) {
      $chunks = array_chunk($context['results']['generate'], $batch_info['max_links']);
326
      foreach ($chunks as $i => $chunk_links) {
327
328
329
330
331
332
        if (count($chunk_links) == $batch_info['max_links']) {
          $remove_sitemap = empty($context['results']['chunk_count']);
          SitemapGenerator::generateSitemap($chunk_links, $remove_sitemap);
          $context['results']['chunk_count'] = !isset($context['results']['chunk_count']) ? 1 : $context['results']['chunk_count'] + 1;
          $context['results']['generate'] = array_slice($context['results']['generate'], count($chunk_links));
        }
333
334
      }
    }
Pawel G's avatar
Pawel G committed
335
336
337
338
339
340
341
342
343
344
345
346
347
  }

  /**
   * Logs and displays an error.
   *
   * @param $message
   *  Untranslated message.
   * @param array $substitutions (optional)
   *  Substitutions (placeholder => substitution) which will replace placeholders
   *  with strings.
   * @param string $type (optional)
   *  Message type (status/warning/error).
   */
Pawel G's avatar
Pawel G committed
348
  private static function registerError($message, $substitutions = [], $type = 'error') {
Pawel G's avatar
Pawel G committed
349
350
351
352
353
    $message = strtr(t($message), $substitutions);
    \Drupal::logger('simple_sitemap')->notice($message);
    drupal_set_message($message, $type);
  }
}