xmlsitemap.module 47 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
    // Removed variables are set to NULL so they can still be deleted.
301
302
303
304
    'xmlsitemap_regenerate_last' => NULL,
    'xmlsitemap_custom_links' => NULL,
    'xmlsitemap_priority_default' => NULL,
    'xmlsitemap_languages' => NULL,
305
306
    'xmlsitemap_max_chunks' => NULL,
    'xmlsitemap_max_filesize' => NULL,
307
308
309
310
311
312
313
314
315
316
  );
}

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

319
320
321
322
323
  $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))));
324
325
  }

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

Darren Oh's avatar
Darren Oh committed
329
/**
330
 * @defgroup xmlsitemap_api XML sitemap API.
331
 * @{
332
333
 * 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
334
 */
335
336
337
338
339
340

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

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

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

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

378
  return $sitemaps;
379
380
381
}

/**
382
 * Load an XML sitemap array from the database based on its context.
383
 *
384
385
 * @param $context
 *   An optional XML sitemap context array to use to find the correct XML
386
 *   sitemap. If not provided, the current site's context will be used.
387
 *
388
 * @see xmlsitemap_get_current_context()
389
 */
390
391
392
function xmlsitemap_sitemap_load_by_context(array $context = NULL) {
  if (!isset($context)) {
    $context = xmlsitemap_get_current_context();
Darren Oh's avatar
Darren Oh committed
393
  }
394
395
  $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();
396
  return xmlsitemap_sitemap_load($smid);
Darren Oh's avatar
Darren Oh committed
397
398
399
}

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

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

418
  // If the context was changed, we need to perform additional actions.
419
  if (!$sitemap->is_new && $sitemap->smid != $sitemap->old_smid) {
420
    // Rename the files directory so the sitemap does not break.
421
    $old_sitemap = (object) array('smid' => $sitemap->old_smid);
422
    $old_dir = xmlsitemap_get_directory($old_sitemap);
423
424
425
426
427
    $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')
428
      ->fields(array('smid' => $sitemap->smid))
429
      ->condition('smid', $sitemap->old_smid)
430
431
432
433
      ->execute();

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

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

  return $sitemap;
446
447
448
}

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

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

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

478
479
480
481
482
483
484
485
/**
 * 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.
 */
486
function xmlsitemap_sitemap_get_file(stdClass $sitemap, $chunk = 'index') {
487
  return xmlsitemap_get_directory($sitemap) . "/{$chunk}.xml";
Darren Oh's avatar
Darren Oh committed
488
489
}

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

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

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

530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
/**
 * 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
545
/**
546
 * Load sitemap links from the database.
547
548
 *
 * @param $conditions
549
550
 *   An array of conditions on the {xmlsitemap} table in the form
 *   'field' => $value.
551
 * @return
552
 *   An array of sitemap link arrays.
Darren Oh's avatar
Darren Oh committed
553
 */
554
function xmlsitemap_link_load_multiple(array $conditions = array()) {
555
556
  $query = db_select('xmlsitemap');
  $query->fields('xmlsitemap');
557

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

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

564
565
566
  return $links;
}

Darren Oh's avatar
Darren Oh committed
567
/**
568
569
 * Saves or updates a sitemap link.
 *
570
 * @param array $link
571
 *   An array with a sitemap link.
572
573
574
575
576
 * @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
577
 */
578
function xmlsitemap_link_save(array $link, array $context = array()) {
579
580
  $link += array(
    'access' => 1,
Dave Reid's avatar
Dave Reid committed
581
    'status' => 1,
582
583
    'status_override' => 0,
    'lastmod' => 0,
584
    'priority' => XMLSITEMAP_PRIORITY_DEFAULT,
585
586
587
    'priority_override' => 0,
    'changefreq' => 0,
    'changecount' => 0,
588
    'language' => LANGUAGE_NONE,
589
590
591
  );

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

  // 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
598
  }
599
600
601
602
603
  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;
  }

604
  $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();
605
606
607
608
609
610

  // 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);
  }

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

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

/**
625
626
627
628
629
630
631
632
633
634
635
 * 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
636
 */
637
function xmlsitemap_link_update_multiple($updates = array(), $conditions = array(), $check_flag = TRUE) {
638
639
  // If we are going to modify a visible sitemap link, we will need to set
  // the regenerate needed flag.
640
  if ($check_flag && !variable_get('xmlsitemap_regenerate_needed', FALSE)) {
641
642
643
644
    _xmlsitemap_check_changed_links($conditions, $updates, TRUE);
  }

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

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

654
/**
655
 * Delete a specific sitemap link from the database.
656
657
658
659
 *
 * If a visible sitemap link was deleted, this will automatically set the
 * regenerate needed flag.
 *
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
 * @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.
 *
678
 * @param $conditions
679
680
 *   An array of conditions on the {xmlsitemap} table in the form
 *   'field' => $value.
681
682
 * @return
 *   The number of links that were deleted.
683
 */
684
function xmlsitemap_link_delete_multiple(array $conditions) {
685
686
687
  // 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().
688
  // @todo Remove this check when http://drupal.org/node/151452 is fixed.
689
690
691
692
  if (!db_table_exists('xmlsitemap')) {
    return FALSE;
  }

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

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

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

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

Darren Oh's avatar
Darren Oh committed
707
/**
708
 * Check if there is a visible sitemap link given a certain set of conditions.
709
 *
710
711
712
713
714
715
716
 * @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.
717
 */
718
719
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.
720
721
  $conditions['status'] = (!empty($updates['status']) && empty($conditions['status'])) ? 0 : 1;
  $conditions['access'] = (!empty($updates['access']) && empty($conditions['access'])) ? 0 : 1;
722

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

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

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

/**
739
 * Check if there is sitemap link is changed from the existing data.
740
 *
741
742
743
744
745
746
747
748
749
750
751
 * @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
752
 */
753
754
function _xmlsitemap_check_changed_link(array $link, $original_link = NULL, $flag = FALSE) {
  $changed = FALSE;
755

756
757
758
759
  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
760

761
762
763
764
  if (!$original_link) {
    if ($link['access'] && $link['status']) {
      // Adding a new visible link.
      $changed = TRUE;
Darren Oh's avatar
Darren Oh committed
765
    }
766
  }
767
768
769
770
771
772
773
774
  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
775
776
    }
  }
777
778
779

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

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

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

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

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

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

/**
805
 * Check that the sitemap files directory exists and is writable.
Darren Oh's avatar
Darren Oh committed
806
 */
807
function xmlsitemap_check_directory(stdClass $sitemap = NULL) {
808
809
810
811
812
813
814
815
  $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;
}

816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
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;
}

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

843
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
/**
 * 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;
}

876
877
878
879
880
881
882
883
/**
 * 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
884
 *   A filepath relative to the Drupal root directory.
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
 * @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();
901
    return $delete_root ? drupal_rmdir($path) : TRUE;
902
903
  }
  return file_unmanaged_delete($path);
Darren Oh's avatar
Darren Oh committed
904
905
906
}

/**
907
908
909
910
911
912
913
914
915
916
917
 * 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
918
 */
919
function xmlsitemap_get_link_info($type = NULL, $reset = FALSE) {
920
  global $language;
921
  $link_info = &drupal_static(__FUNCTION__);
922

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

928
  if (!isset($link_info)) {
929
930
931
    $cid = 'xmlsitemap:link_info:' . $language->language;
    if ($cache = cache_get($cid)) {
      $link_info = $cache->data;
932
    }
933
934
935
936
937
938
939
940
941
942
943
944
945
946
    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]);
          }
        }
947
      }
948
949
950
951
952
953
      $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(),
954
955
          'xmlsitemap' => array(),
        );
956
957
958
959
960
961
962
963
964
        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);
        }
965
      }
966
967
968
969
      drupal_alter('xmlsitemap_link_info', $link_info);
      ksort($link_info);
      // Cache by language since this info contains translated strings.
      cache_set($cid, $link_info);
970
    }
Darren Oh's avatar
Darren Oh committed
971
  }
972
973
974
975
976
977

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

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

980
981
982
983
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) {
984
    $settings = xmlsitemap_link_bundle_load($entity_type, $bundle);
985
986
    if (!empty($settings['status'])) {
    //if (!empty($bundle_info['xmlsitemap']['status'])) {
987
988
989
990
991
992
      $bundles[] = $bundle;
    }
  }
  return $bundles;
}

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

996
997
  $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();
998

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

  return $status;
}
1013

1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
/**
 * 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']);
  }
}

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

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

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

1053
1054
1055
1056
1057
1058
1059
1060
1061
/**
 * 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)) {
1062
      $variable_new = str_replace('xmlsitemap_settings_' . $entity_old, 'xmlsitemap_settings_' . $entity_new, $variable);
1063
1064
1065
1066
1067
1068
1069
1070
      variable_set($variable_new, $value);
    }
  }

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

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

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);
}

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

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

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

    if (!empty($admin['access callback'])) {
      return call_user_func_array($admin['access callback'], $admin['access arguments']);