xmlsitemap.module 47.1 KB
Newer Older
Darren Oh's avatar
Darren Oh committed
1
2
3
<?php

/**
4
 * @defgroup xmlsitemap XML sitemap
Darren Oh's avatar
Darren Oh committed
5
6
7
 */

/**
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 * @file
 * Main file for the xmlsitemap module.
 */

/**
 * The maximum number of links in one sitemap chunk file.
 */
define('XMLSITEMAP_MAX_SITEMAP_LINKS', 50000);

/**
 * The maximum filesize of a sitemap chunk file.
 */
define('XMLSITEMAP_MAX_SITEMAP_FILESIZE', 10485760);

define('XMLSITEMAP_FREQUENCY_YEARLY', 31449600); // 60 * 60 * 24 * 7 * 52
define('XMLSITEMAP_FREQUENCY_MONTHLY', 2419200); // 60 * 60 * 24 * 7 * 4
define('XMLSITEMAP_FREQUENCY_WEEKLY', 604800); // 60 * 60 * 24 * 7
define('XMLSITEMAP_FREQUENCY_DAILY', 86400); // 60 * 60 * 24
define('XMLSITEMAP_FREQUENCY_HOURLY', 3600); // 60 * 60
define('XMLSITEMAP_FREQUENCY_ALWAYS', 60);

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
 * Short lastmod timestamp format.
 */
define('XMLSITEMAP_LASTMOD_SHORT', 'Y-m-d');

/**
 * Medium lastmod timestamp format.
 */
define('XMLSITEMAP_LASTMOD_MEDIUM', 'Y-m-d\TH:i\Z');

/**
 * Long lastmod timestamp format.
 */
define('XMLSITEMAP_LASTMOD_LONG', 'c');

44
45
46
47
48
49
50
51
52
53
/**
 * The default inclusion status for link types in the sitemaps.
 */
define('XMLSITEMAP_STATUS_DEFAULT', 0);

/**
 * The default priority for link types in the sitemaps.
 */
define('XMLSITEMAP_PRIORITY_DEFAULT', 0.5);

54
55
56
57
/**
 * Implements hook_hook_info().
 */
function xmlsitemap_hook_info() {
58
59
60
61
62
63
64
  $hooks = array(
    'xmlsitemap_link_info',
    'xmlsitemap_link_info_alter',
    'xmlsitemap_link_alter',
    'xmlsitemap_index_links',
    'xmlsitemap_context_info',
    'xmlsitemap_context_info_alter',
65
    'xmlsitemap_context_url_options',
66
    'xmlsitemap_context',
67
68
    'xmlsitemap_element_alter',
    'xmlsitemap_root_attributes_alter',
69
70
    'xmlsitemap_sitemap_insert',
    'xmlsitemap_sitemap_update',
71
72
73
74
    'xmlsitemap_sitemap_operations',
    'xmlsitemap_sitemap_delete',
    'xmlsitemap_sitemap_link_url_options_alter',
    'query_xmlsitemap_generate_alter',
75
    'query_xmlsitemap_link_bundle_access_alter',
76
    'form_xmlsitemap_sitemap_edit_form_alter',
77
  );
78
79
80
81
82

  $hooks = array_combine($hooks, $hooks);
  foreach ($hooks as $hook => $info) {
    $hooks[$hook] = array('group' => 'xmlsitemap');
  }
83

84
85
86
  return $hooks;
}

87
88
/**
 * Implements hook_help().
Darren Oh's avatar
Darren Oh committed
89
 */
90
function xmlsitemap_help($path, $arg) {
91
92
  $output = '';

93
  switch ($path) {
94
    case 'admin/help/xmlsitemap':
95
    case 'admin/config/search/xmlsitemap/settings/%/%/%':
Dave Reid's avatar
Dave Reid committed
96
97
    case 'admin/config/search/xmlsitemap/edit/%':
    case 'admin/config/search/xmlsitemap/delete/%':
98
      return;
99
100
    case 'admin/help#xmlsitemap':
      break;
101
    case 'admin/config/search/xmlsitemap':
102
      break;
103
    case 'admin/config/search/xmlsitemap/rebuild':
104
105
106
      $output .= '<p>' . t("This action rebuilds your site's XML sitemap and regenerates the cached files, and may be a lengthy process. If you just installed XML sitemap, this can be helpful to import all your site's content into the sitemap. Otherwise, this should only be used in emergencies.") . '</p>';
  }

107
  if (arg(0) == 'admin' && strpos($path, 'xmlsitemap') !== FALSE && user_access('administer xmlsitemap')) {
108
    module_load_include('inc', 'xmlsitemap');
109
110
111
    if ($arg[1] == 'config') {
      // Alert the user to any potential problems detected by hook_requirements.
      xmlsitemap_check_status();
112
    }
113
    $output .= _xmlsitemap_get_blurb();
Darren Oh's avatar
Darren Oh committed
114
  }
115
116
117
118
119
120
121

  return $output;
}

/**
 * Implements hook_perm().
 */
Dave Reid's avatar
Dave Reid committed
122
function xmlsitemap_permission() {
123
124
  $permissions['administer xmlsitemap'] = array(
    'title' => t('Administer XML sitemap settings.'),
Dave Reid's avatar
Dave Reid committed
125
  );
126
  return $permissions;
Darren Oh's avatar
Darren Oh committed
127
128
129
}

/**
130
 * Implements hook_menu().
Darren Oh's avatar
Darren Oh committed
131
 */
132
function xmlsitemap_menu() {
133
  $items['admin/config/search/xmlsitemap'] = array(
134
    'title' => 'XML sitemap',
135
    'description' => "Configure your site's XML sitemaps to help search engines find and index pages on your site.",
136
    'page callback' => 'drupal_get_form',
137
    'page arguments' => array('xmlsitemap_sitemap_list_form'),
138
139
    'access arguments' => array('administer xmlsitemap'),
    'file' => 'xmlsitemap.admin.inc',
140
  );
141
142
143
144
145
146
147
148
149
150
151
152
  $items['admin/config/search/xmlsitemap/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/config/search/xmlsitemap/add'] = array(
    'title' => 'Add new XML sitemap',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('xmlsitemap_sitemap_edit_form'),
    'access arguments' => array('administer xmlsitemap'),
    'type' => MENU_LOCAL_ACTION,
    'file' => 'xmlsitemap.admin.inc',
153
    'modal' => TRUE,
154
    'options' => array('modal' => TRUE),
155
156
  );
  $items['admin/config/search/xmlsitemap/edit/%xmlsitemap_sitemap'] = array(
157
    'title' => 'Edit XML sitemap',
158
159
160
161
    'page callback' => 'drupal_get_form',
    'page arguments' => array('xmlsitemap_sitemap_edit_form', 5),
    'access arguments' => array('administer xmlsitemap'),
    'file' => 'xmlsitemap.admin.inc',
162
    'modal' => TRUE,
163
164
  );
  $items['admin/config/search/xmlsitemap/delete/%xmlsitemap_sitemap'] = array(
165
    'title' => 'Delete XML sitemap',
166
167
168
169
    'page callback' => 'drupal_get_form',
    'page arguments' => array('xmlsitemap_sitemap_delete_form', 5),
    'access arguments' => array('administer xmlsitemap'),
    'file' => 'xmlsitemap.admin.inc',
170
    'modal' => TRUE,
171
  );
172

173
  $items['admin/config/search/xmlsitemap/settings'] = array(
174
    'title' => 'Settings',
175
176
    'page callback' => 'drupal_get_form',
    'page arguments' => array('xmlsitemap_settings_form'),
177
    'access arguments' => array('administer xmlsitemap'),
178
    'type' => MENU_LOCAL_TASK,
179
    'file' => 'xmlsitemap.admin.inc',
180
    'weight' => 10,
181
  );
182
183
184
185
186
187
188
189
190
191
  $items['admin/config/search/xmlsitemap/settings/%xmlsitemap_link_bundle/%'] = array(
    'load arguments' => array(6),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('xmlsitemap_link_bundle_settings_form', 5),
    'access callback' => 'xmlsitemap_link_bundle_access',
    'access arguments' => array(5),
    'file' => 'xmlsitemap.admin.inc',
    'modal' => TRUE,
  );

192
  $items['admin/config/search/xmlsitemap/rebuild'] = array(
193
    'title' => 'Rebuild links',
194
    'description' => 'Rebuild the site map.',
195
    'page callback' => 'drupal_get_form',
196
    'page arguments' => array('xmlsitemap_rebuild_form'),
197
    'access callback' => '_xmlsitemap_rebuild_form_access',
198
    'type' => MENU_LOCAL_TASK,
199
    'file' => 'xmlsitemap.admin.inc',
200
    'weight' => 20,
201
202
203
  );

  $items['sitemap.xml'] = array(
204
    'page callback' => 'xmlsitemap_output_chunk',
205
    'access callback' => TRUE,
206
207
208
209
210
211
    'type' => MENU_CALLBACK,
    'file' => 'xmlsitemap.pages.inc',
  );
  $items['sitemap.xsl'] = array(
    'page callback' => 'xmlsitemap_output_xsl',
    'access callback' => TRUE,
212
    'type' => MENU_CALLBACK,
213
    'file' => 'xmlsitemap.pages.inc',
214
215
  );

Darren Oh's avatar
Darren Oh committed
216
217
218
  return $items;
}

219
220
221
222
223
224
225
226
227
/**
 * Menu access callback; determines if the user can use the rebuild links page.
 */
function _xmlsitemap_rebuild_form_access() {
  module_load_include('generate.inc', 'xmlsitemap');
  $rebuild_types = xmlsitemap_get_rebuildable_link_types();
  return !empty($rebuild_types) && user_access('administer xmlsitemap');
}

Darren Oh's avatar
Darren Oh committed
228
/**
229
 * Implements hook_cron().
230
231
232
 *
 * @todo Use new Queue system. Need to add {sitemap}.queued.
 * @todo Regenerate one at a time?
Darren Oh's avatar
Darren Oh committed
233
 */
234
function xmlsitemap_cron() {
235
236
  // If there were no new or changed links, skip.
  if (!variable_get('xmlsitemap_regenerate_needed', FALSE)) {
237
238
    return;
  }
239
240
241
242
  // If cron sitemap file regeneration is disabled, stop.
  if (variable_get('xmlsitemap_disable_cron_regeneration', 0)) {
    return;
  }
243

244
245
246
247
248
249
250
  // If the minimum sitemap lifetime hasn't been passed, skip.
  $lifetime = REQUEST_TIME - variable_get('xmlsitemap_generated_last', 0);
  if ($lifetime < variable_get('xmlsitemap_minimum_lifetime', 0)) {
    return;
  }

  // Regenerate the sitemap XML files.
251
  module_load_include('generate.inc', 'xmlsitemap');
252
  xmlsitemap_run_unprogressive_batch('xmlsitemap_regenerate_batch');
Darren Oh's avatar
Darren Oh committed
253
254
}

255
256
257
258
259
260
261
262
263
264
265
266
267
268
/**
 * Implements hook_modules_enabled().
 */
function xmlsitemap_modules_enabled(array $modules) {
  cache_clear_all('xmlsitemap:', 'cache', TRUE);
}

/**
 * Implements hook_modules_disabled().
 */
function xmlsitemap_modules_disabled(array $modules) {
  cache_clear_all('xmlsitemap:', 'cache', TRUE);
}

Darren Oh's avatar
Darren Oh committed
269
/**
270
 * Implements hook_robotstxt().
Darren Oh's avatar
Darren Oh committed
271
 */
272
function xmlsitemap_robotstxt() {
273
  if ($sitemap = xmlsitemap_sitemap_load_by_context()) {
274
    $robotstxt[] = 'Sitemap: ' . url($sitemap->uri['path'], $sitemap->uri['options']);
275
    return $robotstxt;
276
  }
Darren Oh's avatar
Darren Oh committed
277
278
}

279
/**
280
 * Internal default variables for xmlsitemap_var().
281
 */
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
function xmlsitemap_variables() {
  return array(
    'xmlsitemap_rebuild_needed' => FALSE,
    'xmlsitemap_regenerate_needed' => FALSE,
    'xmlsitemap_minimum_lifetime' => 0,
    'xmlsitemap_generated_last' => 0,
    'xmlsitemap_xsl' => 1,
    'xmlsitemap_prefetch_aliases' => 1,
    'xmlsitemap_chunk_size' => 'auto',
    'xmlsitemap_batch_limit' => 100,
    'xmlsitemap_path' => 'xmlsitemap',
    'xmlsitemap_base_url' => $GLOBALS['base_url'],
    'xmlsitemap_developer_mode' => 0,
    'xmlsitemap_frontpage_priority' => 1.0,
    'xmlsitemap_frontpage_changefreq' => XMLSITEMAP_FREQUENCY_DAILY,
    'xmlsitemap_lastmod_format' => XMLSITEMAP_LASTMOD_MEDIUM,
    'xmlsitemap_gz' => FALSE,
299
    'xmlsitemap_disable_cron_regeneration' => 0,
300
    'xmlsitemap_output_elements' => array('lastmod', 'changefreq', 'priority'),
301
    // Removed variables are set to NULL so they can still be deleted.
302
303
304
305
    'xmlsitemap_regenerate_last' => NULL,
    'xmlsitemap_custom_links' => NULL,
    'xmlsitemap_priority_default' => NULL,
    'xmlsitemap_languages' => NULL,
306
307
    'xmlsitemap_max_chunks' => NULL,
    'xmlsitemap_max_filesize' => NULL,
308
309
310
311
312
313
314
315
316
317
  );
}

/**
 * Internal implementation of variable_get().
 */
function xmlsitemap_var($name, $default = NULL) {
  $defaults = &drupal_static(__FUNCTION__);
  if (!isset($defaults)) {
    $defaults = xmlsitemap_variables();
318
319
  }

320
321
322
323
324
  $name = 'xmlsitemap_' . $name;

  // @todo Remove when stable.
  if (!isset($defaults[$name])) {
    trigger_error(strtr('Default variable for %variable not found.', array('%variable' => drupal_placeholder($name))));
325
326
  }

327
  return variable_get($name, isset($default) || !isset($defaults[$name]) ? $default : $defaults[$name]);
328
329
}

Darren Oh's avatar
Darren Oh committed
330
/**
331
 * @defgroup xmlsitemap_api XML sitemap API.
332
 * @{
333
334
 * This is the XML sitemap API to be used by modules wishing to work with
 * XML sitemap and/or link data.
Darren Oh's avatar
Darren Oh committed
335
 */
336
337
338
339
340
341

/**
 * Load an XML sitemap array from the database.
 *
 * @param $smid
 *   An XML sitemap ID.
342
343
344
 *
 * @return
 *   The XML sitemap object.
345
346
347
348
 */
function xmlsitemap_sitemap_load($smid) {
  $sitemap = xmlsitemap_sitemap_load_multiple(array($smid));
  return $sitemap ? reset($sitemap) : FALSE;
Darren Oh's avatar
Darren Oh committed
349
350
351
}

/**
352
353
354
 * Load multiple XML sitemaps from the database.
 *
 * @param $smids
355
356
357
 *   An array of XML sitemap IDs, or FALSE to load all XML sitemaps.
 * @param $conditions
 *   An array of conditions in the form 'field' => $value.
358
359
360
 *
 * @return
 *   An array of XML sitemap objects.
Darren Oh's avatar
Darren Oh committed
361
 */
362
363
364
365
function xmlsitemap_sitemap_load_multiple($smids = array(), array $conditions = array()) {
  if ($smids !== FALSE) {
    $conditions['smid'] = $smids;
  }
366
367
368

  $query = db_select('xmlsitemap_sitemap');
  $query->fields('xmlsitemap_sitemap');
369
370
371
372
  foreach ($conditions as $field => $value) {
    $query->condition($field, $value);
  }

373
  $sitemaps = $query->execute()->fetchAllAssoc('smid');
374
  foreach ($sitemaps as $smid => $sitemap) {
375
376
    $sitemaps[$smid]->context = unserialize($sitemap->context);
    $sitemaps[$smid]->uri = xmlsitemap_sitemap_uri($sitemaps[$smid]);
Darren Oh's avatar
Darren Oh committed
377
  }
378

379
  return $sitemaps;
380
381
382
}

/**
383
 * Load an XML sitemap array from the database based on its context.
384
 *
385
386
 * @param $context
 *   An optional XML sitemap context array to use to find the correct XML
387
 *   sitemap. If not provided, the current site's context will be used.
388
 *
389
 * @see xmlsitemap_get_current_context()
390
 */
391
392
393
function xmlsitemap_sitemap_load_by_context(array $context = NULL) {
  if (!isset($context)) {
    $context = xmlsitemap_get_current_context();
Darren Oh's avatar
Darren Oh committed
394
  }
395
396
  $hash = xmlsitemap_sitemap_get_context_hash($context);
  $smid = db_query_range("SELECT smid FROM {xmlsitemap_sitemap} WHERE smid = :hash", 0, 1, array(':hash' => $hash))->fetchField();
397
  return xmlsitemap_sitemap_load($smid);
Darren Oh's avatar
Darren Oh committed
398
399
400
}

/**
401
 * Save changes to an XML sitemap or add a new XML sitemap.
402
 *
403
 * @param $sitemap
404
 *   The XML sitemap array to be saved. If $sitemap->smid is omitted, a new
405
406
407
 *   XML sitemap will be added.
 *
 * @todo Save the sitemap's URL as a column?
Darren Oh's avatar
Darren Oh committed
408
 */
409
function xmlsitemap_sitemap_save(stdClass $sitemap) {
410
411
412
413
  if (!isset($sitemap->context)) {
    $sitemap->context = array();
  }

414
  // Make sure context is sorted before saving the hash.
415
416
  $sitemap->is_new = empty($sitemap->smid);
  $sitemap->old_smid = $sitemap->is_new ? NULL : $sitemap->smid;
417
  $sitemap->smid = xmlsitemap_sitemap_get_context_hash($sitemap->context);
418

419
  // If the context was changed, we need to perform additional actions.
420
  if (!$sitemap->is_new && $sitemap->smid != $sitemap->old_smid) {
421
    // Rename the files directory so the sitemap does not break.
422
    $old_sitemap = (object) array('smid' => $sitemap->old_smid);
423
    $old_dir = xmlsitemap_get_directory($old_sitemap);
424
425
426
427
428
    $new_dir = xmlsitemap_get_directory($sitemap);
    xmlsitemap_directory_move($old_dir, $new_dir);

    // Change the smid field so drupal_write_record() does not fail.
    db_update('xmlsitemap_sitemap')
429
      ->fields(array('smid' => $sitemap->smid))
430
      ->condition('smid', $sitemap->old_smid)
431
432
433
434
      ->execute();

    // Mark the sitemaps as needing regeneration.
    variable_set('xmlsitemap_regenerate_needed', TRUE);
435
  }
436

437
  if ($sitemap->is_new) {
438
    drupal_write_record('xmlsitemap_sitemap', $sitemap);
439
440
441
442
443
    module_invoke_all('xmlsitemap_sitemap_insert', $sitemap);
  }
  else {
    drupal_write_record('xmlsitemap_sitemap', $sitemap, array('smid'));
    module_invoke_all('xmlsitemap_sitemap_update', $sitemap);
444
  }
445
446

  return $sitemap;
447
448
449
}

/**
450
 * Delete an XML sitemap.
451
 *
452
453
 * @param $smid
 *   An XML sitemap ID.
454
 */
455
456
function xmlsitemap_sitemap_delete($smid) {
  xmlsitemap_sitemap_delete_multiple(array($smid));
457
458
}

Darren Oh's avatar
Darren Oh committed
459
/**
460
 * Delete multiple XML sitemaps.
461
 *
462
463
 * @param $smids
 *   An array of XML sitemap IDs.
Darren Oh's avatar
Darren Oh committed
464
 */
465
466
467
468
469
470
function xmlsitemap_sitemap_delete_multiple(array $smids) {
  if (!empty($smids)) {
    $sitemaps = xmlsitemap_sitemap_load_multiple($smids);
    db_delete('xmlsitemap_sitemap')
        ->condition('smid', $smids)
        ->execute();
471

472
473
474
475
    foreach ($sitemaps as $sitemap) {
      xmlsitemap_clear_directory($sitemap, TRUE);
      module_invoke_all('xmlsitemap_sitemap_delete', $sitemap);
    }
476
  }
477
}
478

479
480
481
482
483
484
485
486
/**
 * Return the expected file path for a specific sitemap chunk.
 *
 * @param $sitemap
 *   An XML sitemap array.
 * @param $chunk
 *   An optional specific chunk in the sitemap. Defaults to the index page.
 */
487
function xmlsitemap_sitemap_get_file(stdClass $sitemap, $chunk = 'index') {
488
  return xmlsitemap_get_directory($sitemap) . "/{$chunk}.xml";
Darren Oh's avatar
Darren Oh committed
489
490
}

491
492
493
494
495
496
/**
 * Find the maximum file size of all a sitemap's XML files.
 *
 * @param $sitemap
 *   The XML sitemap array.
 */
497
function xmlsitemap_sitemap_get_max_filesize(stdClass $sitemap) {
498
  $dir = xmlsitemap_get_directory($sitemap);
499
  $sitemap->max_filesize = 0;
500
  foreach (file_scan_directory($dir, '/\.xml$/') as $file) {
501
    $sitemap->max_filesize = max($sitemap->max_filesize, filesize($file->uri));
502
  }
503
  return $sitemap->max_filesize;
504
505
}

506
507
508
509
510
function xmlsitemap_sitemap_get_context_hash(array &$context) {
  asort($context);
  return drupal_hash_base64(serialize($context));
}

Darren Oh's avatar
Darren Oh committed
511
/**
512
 * Returns the uri elements of an XML sitemap.
513
 *
514
515
 * @param $sitemap
 *   An unserialized data array for an XML sitemap.
516
 * @return
517
518
 *   An array containing the 'path' and 'options' keys used to build the uri of
 *   the XML sitemap, and matching the signature of url().
Darren Oh's avatar
Darren Oh committed
519
 */
520
function xmlsitemap_sitemap_uri(stdClass $sitemap) {
521
  $uri['path'] = 'sitemap.xml';
522
523
  $uri['options'] = module_invoke_all('xmlsitemap_context_url_options', $sitemap->context);
  drupal_alter('xmlsitemap_context_url_options', $uri['options'], $sitemap->context);
524
525
526
527
528
529
  $uri['options'] += array(
    'absolute' => TRUE,
    'base_url' => variable_get('xmlsitemap_base_url', $GLOBALS['base_url']),
  );
  return $uri;
}
530

531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
/**
 * Load a specific sitemap link from the database.
 *
 * @param $entity_type
 *   A string with the entity type.
 * @param $entity_id
 *   An integer with the entity ID.
 * @return
 *   A sitemap link (array) or FALSE if the conditions were not found.
 */
function xmlsitemap_link_load($entity_type, $entity_id) {
  $link = xmlsitemap_link_load_multiple(array('type' => $entity_type, 'id' => $entity_id));
  return $link ? reset($link) : FALSE;
}

Darren Oh's avatar
Darren Oh committed
546
/**
547
 * Load sitemap links from the database.
548
549
 *
 * @param $conditions
550
551
 *   An array of conditions on the {xmlsitemap} table in the form
 *   'field' => $value.
552
 * @return
553
 *   An array of sitemap link arrays.
Darren Oh's avatar
Darren Oh committed
554
 */
555
function xmlsitemap_link_load_multiple(array $conditions = array()) {
556
557
  $query = db_select('xmlsitemap');
  $query->fields('xmlsitemap');
558

559
560
561
  foreach ($conditions as $field => $value) {
    $query->condition($field, $value);
  }
562

563
  $links = $query->execute()->fetchAll(PDO::FETCH_ASSOC);
564

565
566
567
  return $links;
}

Darren Oh's avatar
Darren Oh committed
568
/**
569
570
 * Saves or updates a sitemap link.
 *
571
 * @param array $link
572
 *   An array with a sitemap link.
573
574
575
576
577
 * @param array $context
 *   An optional context array containing data related to the link.
 *
 * @return array
 *   The saved sitemap link.
Darren Oh's avatar
Darren Oh committed
578
 */
579
function xmlsitemap_link_save(array $link, array $context = array()) {
580
581
  $link += array(
    'access' => 1,
Dave Reid's avatar
Dave Reid committed
582
    'status' => 1,
583
584
    'status_override' => 0,
    'lastmod' => 0,
585
    'priority' => XMLSITEMAP_PRIORITY_DEFAULT,
586
587
588
    'priority_override' => 0,
    'changefreq' => 0,
    'changecount' => 0,
589
    'language' => LANGUAGE_NONE,
590
591
592
  );

  // Allow other modules to alter the link before saving.
593
  drupal_alter('xmlsitemap_link', $link, $context);
594
595
596
597
598

  // Temporary validation checks.
  // @todo Remove in final?
  if ($link['priority'] < 0 || $link['priority'] > 1) {
    trigger_error(t('Invalid sitemap link priority %priority.<br />@link', array('%priority' => $link['priority'], '@link' => var_export($link, TRUE))), E_USER_ERROR);
Darren Oh's avatar
Darren Oh committed
599
  }
600
601
602
603
604
  if ($link['changecount'] < 0) {
    trigger_error(t('Negative changecount value. Please report this to <a href="@516928">@516928</a>.<br />@link', array('@516928' => 'http://drupal.org/node/516928', '@link' => var_export($link, TRUE))), E_USER_ERROR);
    $link['changecount'] = 0;
  }

605
  $existing = db_query_range("SELECT loc, access, status, lastmod, priority, changefreq, changecount, language FROM {xmlsitemap} WHERE type = :type AND id = :id", 0, 1, array(':type' => $link['type'], ':id' => $link['id']))->fetchAssoc();
606
607
608
609
610
611

  // Check if this is a changed link and set the regenerate flag if necessary.
  if (!variable_get('xmlsitemap_regenerate_needed', FALSE)) {
    _xmlsitemap_check_changed_link($link, $existing, TRUE);
  }

612
  // Save the link and allow other modules to respond to the link being saved.
613
  if ($existing) {
614
    drupal_write_record('xmlsitemap', $link, array('type', 'id'));
615
    module_invoke_all('xmlsitemap_link_update', $link, $context);
616
617
  }
  else {
618
    drupal_write_record('xmlsitemap', $link);
619
    module_invoke_all('xmlsitemap_link_insert', $link, $context);
620
621
622
  }

  return $link;
Darren Oh's avatar
Darren Oh committed
623
624
625
}

/**
626
627
628
629
630
631
632
633
634
635
636
 * Perform a mass update of sitemap data.
 *
 * If visible links are updated, this will automatically set the regenerate
 * needed flag to TRUE.
 *
 * @param $updates
 *   An array of values to update fields to, keyed by field name.
 * @param $conditions
 *   An array of values to match keyed by field.
 * @return
 *   The number of links that were updated.
Darren Oh's avatar
Darren Oh committed
637
 */
638
function xmlsitemap_link_update_multiple($updates = array(), $conditions = array(), $check_flag = TRUE) {
639
640
  // If we are going to modify a visible sitemap link, we will need to set
  // the regenerate needed flag.
641
  if ($check_flag && !variable_get('xmlsitemap_regenerate_needed', FALSE)) {
642
643
644
645
    _xmlsitemap_check_changed_links($conditions, $updates, TRUE);
  }

  // Process updates.
646
647
648
649
650
  $query = db_update('xmlsitemap');
  $query->fields($updates);
  foreach ($conditions as $field => $value) {
    $query->condition($field, $value);
  }
651

652
  return $query->execute();
Darren Oh's avatar
Darren Oh committed
653
654
}

655
/**
656
 * Delete a specific sitemap link from the database.
657
658
659
660
 *
 * If a visible sitemap link was deleted, this will automatically set the
 * regenerate needed flag.
 *
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
 * @param $entity_type
 *   A string with the entity type.
 * @param $entity_id
 *   An integer with the entity ID.
 * @return
 *   The number of links that were deleted.
 */
function xmlsitemap_link_delete($entity_type, $entity_id) {
  $conditions = array('type' => $entity_type, 'id' => $entity_id);
  return xmlsitemap_link_delete_multiple($conditions);
}

/**
 * Delete multiple sitemap links from the database.
 *
 * If visible sitemap links were deleted, this will automatically set the
 * regenerate needed flag.
 *
679
 * @param $conditions
680
681
 *   An array of conditions on the {xmlsitemap} table in the form
 *   'field' => $value.
682
683
 * @return
 *   The number of links that were deleted.
684
 */
685
function xmlsitemap_link_delete_multiple(array $conditions) {
686
687
688
  // Because this function is called from sub-module uninstall hooks, we have
  // to manually check if the table exists since it could have been removed
  // in xmlsitemap_uninstall().
689
  // @todo Remove this check when http://drupal.org/node/151452 is fixed.
690
691
692
693
  if (!db_table_exists('xmlsitemap')) {
    return FALSE;
  }

694
695
  if (!variable_get('xmlsitemap_regenerate_needed', TRUE)) {
    _xmlsitemap_check_changed_links($conditions, array(), TRUE);
696
  }
697

698
699
  // @todo Add a hook_xmlsitemap_link_delete() hook invoked here.

700
701
702
703
  $query = db_delete('xmlsitemap');
  foreach ($conditions as $field => $value) {
    $query->condition($field, $value);
  }
704

705
  return $query->execute();
706
707
}

Darren Oh's avatar
Darren Oh committed
708
/**
709
 * Check if there is a visible sitemap link given a certain set of conditions.
710
 *
711
712
713
714
715
716
717
 * @param $conditions
 *   An array of values to match keyed by field.
 * @param $flag
 *   An optional boolean that if TRUE, will set the regenerate needed flag if
 *   there is a match. Defaults to FALSE.
 * @return
 *   TRUE if there is a visible link, or FALSE otherwise.
718
 */
719
720
function _xmlsitemap_check_changed_links(array $conditions = array(), array $updates = array(), $flag = FALSE) {
  // If we are changing status or access, check for negative current values.
721
722
  $conditions['status'] = (!empty($updates['status']) && empty($conditions['status'])) ? 0 : 1;
  $conditions['access'] = (!empty($updates['access']) && empty($conditions['access'])) ? 0 : 1;
723

724
725
726
727
  $query = db_select('xmlsitemap');
  $query->addExpression('1');
  foreach ($conditions as $field => $value) {
    $query->condition($field, $value);
728
  }
729
730
  $query->range(0, 1);
  $changed = $query->execute()->fetchField();
731

732
733
  if ($changed && $flag) {
    variable_set('xmlsitemap_regenerate_needed', TRUE);
Darren Oh's avatar
Darren Oh committed
734
  }
735

736
  return $changed;
Darren Oh's avatar
Darren Oh committed
737
738
739
}

/**
740
 * Check if there is sitemap link is changed from the existing data.
741
 *
742
743
744
745
746
747
748
749
750
751
752
 * @param $link
 *   An array of the sitemap link.
 * @param $original_link
 *   An optional array of the existing data. This should only contain the
 *   fields necessary for comparison. If not provided the existing data will be
 *   loaded from the database.
 * @param $flag
 *   An optional boolean that if TRUE, will set the regenerate needed flag if
 *   there is a match. Defaults to FALSE.
 * @return
 *   TRUE if the link is changed, or FALSE otherwise.
Darren Oh's avatar
Darren Oh committed
753
 */
754
755
function _xmlsitemap_check_changed_link(array $link, $original_link = NULL, $flag = FALSE) {
  $changed = FALSE;
756

757
758
759
760
  if ($original_link === NULL) {
    // Load only the fields necessary for data to be changed in the sitemap.
    $original_link = db_query_range("SELECT loc, access, status, lastmod, priority, changefreq, changecount, language FROM {xmlsitemap} WHERE type = :type AND id = :id", 0, 1, array(':type' => $link['type'], ':id' => $link['id']))->fetchAssoc();
  }
Darren Oh's avatar
Darren Oh committed
761

762
763
764
765
  if (!$original_link) {
    if ($link['access'] && $link['status']) {
      // Adding a new visible link.
      $changed = TRUE;
Darren Oh's avatar
Darren Oh committed
766
    }
767
  }
768
769
770
771
772
773
774
775
  else {
    if (!($original_link['access'] && $original_link['status']) && $link['access'] && $link['status']) {
      // Changing a non-visible link to a visible link.
      $changed = TRUE;
    }
    elseif ($original_link['access'] && $original_link['status'] && array_diff_assoc($original_link, $link)) {
      // Changing a visible link
      $changed = TRUE;
Darren Oh's avatar
Darren Oh committed
776
777
    }
  }
778
779
780

  if ($changed && $flag) {
    variable_set('xmlsitemap_regenerate_needed', TRUE);
781
782
  }

783
  return $changed;
Darren Oh's avatar
Darren Oh committed
784
785
786
}

/**
787
 * @} End of "defgroup xmlsitemap_api"
788
 */
Darren Oh's avatar
Darren Oh committed
789

790
function xmlsitemap_get_directory(stdClass $sitemap = NULL) {
791
  $directory = &drupal_static(__FUNCTION__);
792

793
  if (!isset($directory)) {
794
    $directory = variable_get('xmlsitemap_path', 'xmlsitemap');
Darren Oh's avatar
Darren Oh committed
795
  }
796

797
798
  if (!empty($sitemap->smid)) {
    return file_build_uri($directory . '/' . $sitemap->smid);
799
800
801
802
  }
  else {
    return file_build_uri($directory);
  }
Darren Oh's avatar
Darren Oh committed
803
804
805
}

/**
806
 * Check that the sitemap files directory exists and is writable.
Darren Oh's avatar
Darren Oh committed
807
 */
808
function xmlsitemap_check_directory(stdClass $sitemap = NULL) {
809
810
811
812
813
814
815
816
  $directory = xmlsitemap_get_directory($sitemap);
  $result = file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
  if (!$result) {
    watchdog('file system', 'The directory %directory does not exist or is not writable.', array('%directory' => $directory), WATCHDOG_ERROR);
  }
  return $result;
}

817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
function xmlsitemap_check_all_directories() {
  $directories = array();

  $sitemaps = xmlsitemap_sitemap_load_multiple(FALSE);
  foreach ($sitemaps as $smid => $sitemap) {
    $directory = xmlsitemap_get_directory($sitemap);
    $directories[$directory] = $directory;
  }

  foreach ($directories as $directory) {
    $result = file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
    if ($result) {
      $directories[$directory] = TRUE;
    }
    else {
      $directories[$directory] = FALSE;
    }
  }

  return $directories;
}

839
function xmlsitemap_clear_directory(stdClass $sitemap = NULL, $delete = FALSE) {
840
841
842
843
  $directory = xmlsitemap_get_directory($sitemap);
  return _xmlsitemap_delete_recursive($directory, $delete);
}

844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
/**
 * Move a directory to a new location.
 *
 * @param $old_dir
 *   A string specifying the filepath or URI of the original directory.
 * @param $new_dir
 *   A string specifying the filepath or URI of the new directory.
 * @param $replace
 *   Replace behavior when the destination file already exists.
 *
 * @return
 *   TRUE if the directory was moved successfully. FALSE otherwise.
 */
function xmlsitemap_directory_move($old_dir, $new_dir, $replace = FILE_EXISTS_REPLACE) {
  $success = file_prepare_directory($new_dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);

  $old_path = drupal_realpath($old_dir);
  $new_path = drupal_realpath($new_dir);
  if (!is_dir($old_path) || !is_dir($new_path) || !$success) {
    return FALSE;
  }

  $files = file_scan_directory($old_dir, '/.*/');
  foreach ($files as $file) {
    $file->uri_new = $new_dir . '/' . basename($file->filename);
    $success &= (bool) file_unmanaged_move($file->uri, $file->uri_new, $replace);
  }

  // The remove the directory.
  $success &= drupal_rmdir($old_dir);
  return $success;
}

877
878
879
880
881
882
883
884
/**
 * Recursively delete all files and folders in the specified filepath.
 *
 * This is a backport of Drupal 7's file_unmanaged_delete_recursive().
 *
 * Note that this only deletes visible files with write permission.
 *
 * @param $path
885
 *   A filepath relative to the Drupal root directory.
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
 * @param $delete_root
 *   A boolean if TRUE will delete the $path directory afterwards.
 */
function _xmlsitemap_delete_recursive($path, $delete_root = FALSE) {
  // Resolve streamwrapper URI to local path.
  $path = drupal_realpath($path);
  if (is_dir($path)) {
    $dir = dir($path);
    while (($entry = $dir->read()) !== FALSE) {
      if ($entry == '.' || $entry == '..') {
        continue;
      }
      $entry_path = $path . '/' . $entry;
      file_unmanaged_delete_recursive($entry_path, TRUE);
    }
    $dir->close();
902
    return $delete_root ? drupal_rmdir($path) : TRUE;
903
904
  }
  return file_unmanaged_delete($path);
Darren Oh's avatar
Darren Oh committed
905
906
907
}

/**
908
909
910
911
912
913
914
915
916
917
918
 * Returns information about supported sitemap link types.
 *
 * @param $type
 *   (optional) The link type to return information for. If omitted,
 *   information for all link types is returned.
 * @param $reset
 *   (optional) Boolean whether to reset the static cache and do nothing. Only
 *   used for tests.
 *
 * @see hook_xmlsitemap_link_info()
 * @see hook_xmlsitemap_link_info_alter()
Darren Oh's avatar
Darren Oh committed
919
 */
920
function xmlsitemap_get_link_info($type = NULL, $reset = FALSE) {
921
  global $language;
922
  $link_info = &drupal_static(__FUNCTION__);
923

924
925
  if ($reset) {
    $link_info = NULL;
926
    cache_clear_all('xmlsitemap:link_info:', 'cache', TRUE);
927
928
  }

929
  if (!isset($link_info)) {
930
931
932
    $cid = 'xmlsitemap:link_info:' . $language->language;
    if ($cache = cache_get($cid)) {
      $link_info = $cache->data;
933
    }
934
935
936
937
938
939
940
941
942
943
944
945
946
947
    else {
      entity_info_cache_clear();
      $link_info = entity_get_info();
      foreach ($link_info as $key => $info) {
        if (empty($info['uri callback']) || !isset($info['xmlsitemap'])) {
          // Remove any non URL-able or XML sitemap un-supported entites.
          unset($link_info[$key]);
        }
        foreach ($info['bundles'] as $bundle_key => $bundle) {
          if (!isset($bundle['xmlsitemap'])) {
            // Remove any un-supported entity bundles.
            //unset($link_info[$key]['bundles'][$bundle_key]);
          }
        }
948
      }
949
950
951
952
953
954
      $link_info = array_merge($link_info, module_invoke_all('xmlsitemap_link_info'));
      foreach ($link_info as $key => &$info) {
        $info += array(
          'type' => $key,
          'base table' => FALSE,
          'bundles' => array(),
955
956
          'xmlsitemap' => array(),
        );
957
958
959
960
961
962
963
964
965
        if (!isset($info['xmlsitemap']['rebuild callback']) && !empty($info['base table']) && !empty($info['entity keys']['id']) && !empty($info['xmlsitemap']['process callback'])) {
          $info['xmlsitemap']['rebuild callback'] = 'xmlsitemap_rebuild_batch_fetch';
        }
        foreach ($info['bundles'] as $bundle => &$bundle_info) {
          $bundle_info += array(
            'xmlsitemap' => array(),
          );
          $bundle_info['xmlsitemap'] += xmlsitemap_link_bundle_load($key, $bundle, FALSE);
        }
966
      }
967
968
969
970
      drupal_alter('xmlsitemap_link_info', $link_info);
      ksort($link_info);
      // Cache by language since this info contains translated strings.
      cache_set($cid, $link_info);
971
    }
Darren Oh's avatar
Darren Oh committed
972
  }
973
974
975
976
977
978

  if (isset($type)) {
    return isset($link_info[$type]) ? $link_info[$type] : NULL;
  }

  return $link_info;
Darren Oh's avatar
Darren Oh committed
979
980
}

981
982
983
984
function xmlsitemap_get_link_type_enabled_bundles($entity_type) {
  $bundles = array();
  $info = xmlsitemap_get_link_info($entity_type);
  foreach ($info['bundles'] as $bundle => $bundle_info) {
985
    $settings = xmlsitemap_link_bundle_load($entity_type, $bundle);
986
987
    if (!empty($settings['status'])) {
    //if (!empty($bundle_info['xmlsitemap']['status'])) {
988
989
990
991
992
993
      $bundles[] = $bundle;
    }
  }
  return $bundles;
}

994
995
996
function xmlsitemap_get_link_type_indexed_status($entity_type, $bundle = '') {
  $info = xmlsitemap_get_link_info($entity_type);

997
998
  $status['indexed'] = db_query("SELECT COUNT(id) FROM {xmlsitemap} WHERE type = :entity AND subtype = :bundle", array(':entity' => $entity_type, ':bundle' => $bundle))->fetchField();
  $status['visible'] = db_query("SELECT COUNT(id) FROM {xmlsitemap} WHERE type = :entity AND subtype = :bundle AND status = 1 AND access = 1", array(':entity' => $entity_type, ':bundle' => $bundle))->fetchField();
999

1000
1001
1002
1003
1004
  $total = new EntityFieldQuery();
  $total->entityCondition('entity_type', $entity_type);
  $total->entityCondition('bundle', $bundle);
  $total->entityCondition('entity_id', 0, '>');
  //$total->addTag('xmlsitemap_link_bundle_access');
1005
  $total->addTag('xmlsitemap_link_indexed_status');
1006
1007
1008
  $total->addMetaData('entity', $entity_type);
  $total->addMetaData('bundle', $bundle);
  $total->addMetaData('entity_info', $info);
1009
1010
  $total->count();
  $status['total'] = $total->execute();
1011
1012
1013

  return $status;
}
1014

1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
/**
 * Implements hook_entity_query_alter().
 *
 * @todo Remove when http://drupal.org/node/1054168 is fixed.
 */
function xmlsitemap_entity_query_alter($query) {
  $conditions = &$query->entityConditions;

  // Alter user entity queries only.
  if (isset($conditions['entity_type']) && $conditions['entity_type']['value'] == 'user' && isset($conditions['bundle'])) {
    unset($conditions['bundle']);
  }
}

1029
1030
function xmlsitemap_link_bundle_settings_save($entity, $bundle, array $settings, $update_links = TRUE) {
  if ($update_links) {
1031
    $old_settings = xmlsitemap_link_bundle_load($entity, $bundle);
1032
    if ($settings['status'] != $old_settings['status']) {
1033
      xmlsitemap_link_update_multiple(array('status' => $settings['status']), array('type' => $entity, 'subtype' => $bundle, 'status_override' => 0));
1034
1035
    }
    if ($settings['priority'] != $old_settings['priority']) {
1036
      xmlsitemap_link_update_multiple(array('priority' => $settings['priority']), array('type' => $entity, 'subtype' => $bundle, 'priority_override' => 0));
1037
1038
1039
1040
1041
1042
1043
1044
    }
  }

  variable_set("xmlsitemap_settings_{$entity}_{$bundle}", $settings);
  cache_clear_all('xmlsitemap:link_info:', 'cache', TRUE);
  //xmlsitemap_get_link_info(NULL, TRUE);
}

1045
1046
function xmlsitemap_link_bundle_rename($entity, $bundle_old, $bundle_new) {
  if ($bundle_old != $bundle_new) {
1047
    $settings = xmlsitemap_link_bundle_load($entity, $bundle_old);
1048
1049
    variable_del("xmlsitemap_settings_{$entity}_{$bundle_old}");
    xmlsitemap_link_bundle_settings_save($entity, $bundle_new, $settings, FALSE);
1050
    xmlsitemap_link_update_multiple(array('subtype' => $bundle_new), array('type' => $entity, 'subtype' => $bundle_old));
1051
1052
1053
  }
}

1054
1055
1056
1057
1058
1059
1060
1061
1062
/**
 * Rename a link type.
 */
function xmlsitemap_link_type_rename($entity_old, $entity_new, $bundles = NULL) {
  $variables = db_query("SELECT name FROM {variable} WHERE name LIKE :pattern", array(':pattern' => db_like('xmlsitemap_settings_' . $entity_old . '_') . '%'))->fetchCol();
  foreach ($variables as $variable) {
    $value = variable_get($variable);
    variable_del($variable);
    if (isset($value)) {
1063
      $variable_new = str_replace('xmlsitemap_settings_' . $entity_old, 'xmlsitemap_settings_' . $entity_new, $variable);
1064
1065
1066
1067
1068
1069
1070
1071
      variable_set($variable_new, $value);
    }
  }

  xmlsitemap_link_update_multiple(array('type' => $entity_new), array('type' => $entity_old), FALSE);
  xmlsitemap_get_link_info(NULL, TRUE);
}

1072
1073
1074
1075
1076
1077
1078
function xmlsitemap_link_bundle_load($entity, $bundle, $load_bundle_info = TRUE) {
  $info = array(
    'entity' => $entity,
    'bundle' => $bundle,
  );
  if ($load_bundle_info) {
    $entity_info = xmlsitemap_get_link_info($entity);
1079
1080
1081
    if (isset($entity_info['bundles'][$bundle])) {
      $info['info'] = $entity_info['bundles'][$bundle];
    }
1082
1083
1084
1085
1086
1087
1088
  }
  $info += variable_get("xmlsitemap_settings_{$entity}_{$bundle}", array());
  $info += array(
    'status' => XMLSITEMAP_STATUS_DEFAULT,
    'priority' => XMLSITEMAP_PRIORITY_DEFAULT,
  );
  return $info;
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
}

function xmlsitemap_link_bundle_delete($entity, $bundle, $delete_links = TRUE) {
  variable_del("xmlsitemap_settings_{$entity}_{$bundle}");
  if ($delete_links) {
    xmlsitemap_link_delete_multiple(array('type' => $entity, 'subtype' => $bundle));
  }
  cache_clear_all('xmlsitemap:link_info:', 'cache', TRUE);
  //xmlsitemap_get_link_info(NULL, TRUE);
}

1100
1101
1102
1103
1104
1105
1106
function xmlsitemap_link_bundle_access($entity, $bundle = NULL) {
  if (is_array($entity) && !isset($bundle)) {
    $bundle = $entity;
  }
  else {
    $bundle = xmlsitemap_link_bundle_load($entity, $bundle);
  }
1107

1108
1109
  if (isset($bundle['info']['admin'])) {
    $admin = $bundle['info']['admin'];
1110
1111
1112
1113
1114
1115
1116
1117
    $admin += array('access arguments' => array());

    if (!isset($admin['access callback']) && count($admin['access arguments']) == 1) {
      $admin['access callback'] = 'user_access';
    }