Projet

Général

Profil

Paste
Télécharger (22,1 ko) Statistiques
| Branche: | Révision:

root / htmltest / sites / all / modules / piwik / piwik.module @ 3cb08e71

1
<?php
2

    
3
/**
4
 * @file
5
 * Drupal Module: Piwik
6
 * Adds the required Javascript to the bottom of all your Drupal pages
7
 * to allow tracking by the Piwik statistics package.
8
 *
9
 * @author: Alexander Hass <http://drupal.org/user/85918>
10
 */
11

    
12
define('PK_TRACKFILES_EXTENSIONS', '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls|xml|z|zip');
13

    
14
/**
15
 * Implements hook_help().
16
 */
17
function piwik_help($path, $arg) {
18
  switch ($path) {
19
    case 'admin/config/system/piwik':
20
      return t('<a href="@pk_url">Piwik - Web analytics</a> is an open source (GPL license) web analytics software. It gives interesting reports on your website visitors, your popular pages, the search engines keywords they used, the language they speak... and so much more. Piwik aims to be an open source alternative to Google Analytics.', array('@pk_url' => 'http://www.piwik.org/'));
21
  }
22
}
23

    
24
/**
25
 * Implements hook_theme().
26
 */
27
function piwik_theme() {
28
  return array(
29
    'piwik_admin_custom_var_table' => array(
30
      'render element' => 'form',
31
    ),
32
  );
33
}
34

    
35
/**
36
 * Implements hook_permission().
37
 */
38
function piwik_permission() {
39
  return array(
40
    'administer piwik' => array(
41
      'title' => t('Administer Piwik'),
42
      'description' => t('Perform maintenance tasks for Piwik.'),
43
    ),
44
    'opt-in or out of tracking' => array(
45
      'title' => t('Opt-in or out of tracking'),
46
      'description' => t('Allow users to decide if tracking code will be added to pages or not.'),
47
    ),
48
    'use PHP for tracking visibility' => array(
49
      'title' => t('Use PHP for tracking visibility'),
50
      'description' => t('Enter PHP code in the field for tracking visibility settings.'),
51
      'restrict access' => TRUE,
52
    ),
53
  );
54
}
55

    
56
/**
57
 * Implements hook_menu().
58
 */
59
function piwik_menu() {
60
  $items['admin/config/system/piwik'] = array(
61
    'title' => 'Piwik',
62
    'description' => 'Configure the settings used to generate your Piwik tracking code.',
63
    'page callback' => 'drupal_get_form',
64
    'page arguments' => array('piwik_admin_settings_form'),
65
    'access arguments' => array('administer piwik'),
66
    'type' => MENU_NORMAL_ITEM,
67
    'file' => 'piwik.admin.inc',
68
  );
69

    
70
  return $items;
71
}
72

    
73
/**
74
 * Implements hook_page_alter() to insert JavaScript to the appropriate scope/region of the page.
75
 */
76
function piwik_page_alter(&$page) {
77
  global $user;
78

    
79
  $id = variable_get('piwik_site_id', '');
80

    
81
  // Get page status code for visibility filtering.
82
  $status = drupal_get_http_header('Status');
83
  $trackable_status_codes = array(
84
    '403 Forbidden',
85
    '404 Not Found',
86
  );
87

    
88
  // 1. Check if the piwik account number has a value.
89
  // 2. Track page views based on visibility value.
90
  // 3. Check if we should track the currently active user's role.
91
  if (!empty($id) && (_piwik_visibility_pages() || in_array($status, $trackable_status_codes)) && _piwik_visibility_user($user)) {
92

    
93
    $url_http = variable_get('piwik_url_http', '');
94
    $url_https = variable_get('piwik_url_https', '');
95
    $scope = variable_get('piwik_js_scope', 'footer');
96

    
97
    $set_custom_url = '';
98
    $set_document_title = '';
99
    $set_custom_data = array();
100

    
101
    // Piwik can show a tree view of page titles that represents the site structure
102
    // if setDocumentTitle() provides the page titles as a "/" delimited list.
103
    // This may makes it easier to browse through the statistics of page titles
104
    // on larger sites.
105
    if (variable_get('piwik_page_title_hierarchy', FALSE) == TRUE) {
106
      $titles = _piwik_get_hierarchy_titles();
107

    
108
      if (variable_get('piwik_page_title_hierarchy_exclude_home', TRUE)) {
109
        // Remove the "Home" item from the titles to flatten the tree view.
110
        array_shift($titles);
111
      }
112

    
113
      if (!empty($titles)) {
114
        // Encode title, at least to keep "/" intact.
115
        $titles = array_map('urlencode', $titles);
116

    
117
        $set_document_title = drupal_json_encode(implode('/', $titles));
118
      }
119
    }
120

    
121
    // If this node is a translation of another node, pass the original
122
    // node instead.
123
    if (module_exists('translation') && variable_get('piwik_translation_set', 0)) {
124
      // Check we have a node object, it supports translation, and its
125
      // translated node ID (tnid) doesn't match its own node ID.
126
      $node = menu_get_object();
127
      if ($node && translation_supported_type($node->type) && !empty($node->tnid) && ($node->tnid != $node->nid)) {
128
        $source_node = node_load($node->tnid);
129
        $languages = language_list();
130
        $set_custom_url = drupal_json_encode(url('node/' . $source_node->nid, array('language' => $languages[$source_node->language], 'absolute' => TRUE)));
131
      }
132
    }
133

    
134
    // Track access denied (403) and file not found (404) pages.
135
    if ($status == '403 Forbidden') {
136
      $set_document_title = '"403/URL = " + String(document.location.pathname + document.location.search).replace(/\//g,"%2f") + "/From = " + String(document.referrer).replace(/\//g,"%2f")';
137
    }
138
    elseif ($status == '404 Not Found') {
139
      $set_document_title = '"404/URL = " + String(document.location.pathname + document.location.search).replace(/\//g,"%2f") + "/From = " + String(document.referrer).replace(/\//g,"%2f")';
140
    }
141

    
142
    // Add custom variables.
143
    $piwik_custom_vars = variable_get('piwik_custom_var', array());
144
    $custom_variable = '';
145
    for ($i = 1; $i < 6; $i++) {
146
      $custom_var_name = !empty($piwik_custom_vars['slots'][$i]['name']) ? $piwik_custom_vars['slots'][$i]['name'] : '';
147
      if (!empty($custom_var_name)) {
148
        $custom_var_value = !empty($piwik_custom_vars['slots'][$i]['value']) ? $piwik_custom_vars['slots'][$i]['value'] : '';
149
        $custom_var_scope = !empty($piwik_custom_vars['slots'][$i]['scope']) ? $piwik_custom_vars['slots'][$i]['scope'] : 'visit';
150

    
151
        $types = array();
152
        $node = menu_get_object();
153
        if (is_object($node)) {
154
          $types += array('node' => $node);
155
        }
156
        $custom_var_name = token_replace($custom_var_name, $types, array('clear' => TRUE));
157
        $custom_var_value = token_replace($custom_var_value, $types, array('clear' => TRUE));
158

    
159
        // Suppress empty custom names and/or variables.
160
        if (!drupal_strlen(trim($custom_var_name)) || !drupal_strlen(trim($custom_var_value))) {
161
          continue;
162
        }
163

    
164
        // Custom variables names and values are limited to 200 characters in
165
        // length. It is recommended to store values that are as small as
166
        // possible to ensure that the Piwik Tracking request URL doesn't go
167
        // over the URL limit for the webserver or browser.
168
        $custom_var_name = rtrim(substr($custom_var_name, 0, 200));
169
        $custom_var_value = rtrim(substr($custom_var_value, 0, 200));
170

    
171
        $custom_var_name = drupal_json_encode($custom_var_name);
172
        $custom_var_value = drupal_json_encode($custom_var_value);
173
        $custom_var_scope = drupal_json_encode($custom_var_scope);
174
        $custom_variable .= "_paq.push(['setCustomVariable', $i, $custom_var_name, $custom_var_value, $custom_var_scope]);";
175
      }
176
    }
177

    
178
    // Add any custom code snippets if specified.
179
    $codesnippet_before = variable_get('piwik_codesnippet_before', '');
180
    $codesnippet_after = variable_get('piwik_codesnippet_after', '');
181

    
182
    // Build tracker code. See http://piwik.org/docs/javascript-tracking/#toc-asynchronous-tracking
183
    $script = 'var _paq = _paq || [];';
184
    $script .= '(function(){';
185
    $script .= 'var u=(("https:" == document.location.protocol) ? "' . check_url($url_https) . '" : "' . check_url($url_http) . '");';
186
    $script .= '_paq.push(["setSiteId", ' . drupal_json_encode(variable_get('piwik_site_id', '')) . ']);';
187
    $script .= '_paq.push(["setTrackerUrl", u+"piwik.php"]);';
188

    
189
    // Set custom data.
190
    if (!empty($set_custom_data)) {
191
      foreach ($set_custom_data as $custom_data) {
192
        $script .= '_paq.push(["setCustomData", ' . $custom_data . ']);';
193
      }
194
    }
195
    // Set custom url.
196
    if (!empty($set_custom_url)) {
197
      $script .= '_paq.push(["setCustomUrl", ' . $set_custom_url . ']);';
198
    }
199
    // Set custom document title.
200
    if (!empty($set_document_title)) {
201
      $script .= '_paq.push(["setDocumentTitle", ' . $set_document_title . ']);';
202
    }
203

    
204
    // Custom file download extensions.
205
    if ((variable_get('piwik_track', 1)) && !(variable_get('piwik_trackfiles_extensions', PK_TRACKFILES_EXTENSIONS) == PK_TRACKFILES_EXTENSIONS)) {
206
      $script .= '_paq.push(["setDownloadExtensions", ' . drupal_json_encode(variable_get('piwik_trackfiles_extensions', PK_TRACKFILES_EXTENSIONS)) . ']);';
207
    }
208

    
209
    // Disable tracking for visitors who have opted out from tracking via DNT (Do-Not-Track) header.
210
    if (variable_get('piwik_privacy_donottrack', 1)) {
211
      $script .= '_paq.push(["setDoNotTrack", 1]);';
212
    }
213

    
214
    // Domain tracking type.
215
    global $cookie_domain;
216
    $domain_mode = variable_get('piwik_domain_mode', 0);
217

    
218
    // Per RFC 2109, cookie domains must contain at least one dot other than the
219
    // first. For hosts such as 'localhost' or IP Addresses we don't set a cookie domain.
220
    if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
221
      $script .= '_paq.push(["setCookieDomain", ' . drupal_json_encode($cookie_domain) . ']);';
222
    }
223

    
224
    // Ordering $custom_variable before $codesnippet_before allows users to add
225
    // custom code snippets that may use deleteCustomVariable() and/or getCustomVariable().
226
    if (!empty($custom_variable)) {
227
      $script .= $custom_variable;
228
    }
229
    if (!empty($codesnippet_before)) {
230
      $script .= $codesnippet_before;
231
    }
232

    
233
    // Site search tracking support.
234
    // NOTE: It's recommended not to call trackPageView() on the Site Search Result page.
235
    if (module_exists('search') && variable_get('piwik_site_search', FALSE) && arg(0) == 'search' && $keys = piwik_search_get_keys()) {
236
      // Parameters:
237
      // 1. Search keyword searched for. Example: "Banana"
238
      // 2. Search category selected in your search engine. If you do not need
239
      //    this, set to false. Example: "Organic Food"
240
      // 3. Number of results on the Search results page. Zero indicates a
241
      //    'No Result Search Keyword'. Set to false if you don't know.
242
      //
243
      // hook_preprocess_search_results() is not executed if search result is
244
      // empty. Make sure the counter is set to 0 if there are no results.
245
      $script .= '_paq.push(["trackSiteSearch", ' . drupal_json_encode($keys) . ', false, (window.piwik_search_results) ? window.piwik_search_results : 0]);';
246
    }
247
    else {
248
      $script .= '_paq.push(["trackPageView"]);';
249
    }
250

    
251
    // Add link tracking.
252
    if (variable_get('piwik_track', 1)) {
253
      $script .= '_paq.push(["enableLinkTracking"]);';
254
    }
255
    if (!empty($codesnippet_after)) {
256
      $script .= $codesnippet_after;
257
    }
258

    
259
    $script .= 'var d=document,';
260
    $script .= 'g=d.createElement("script"),';
261
    $script .= 's=d.getElementsByTagName("script")[0];';
262
    $script .= 'g.type="text/javascript";';
263
    $script .= 'g.defer=true;';
264
    $script .= 'g.async=true;';
265

    
266
    // Should a local cached copy of the tracking code be used?
267
    if (variable_get('piwik_cache', 0) && $url = _piwik_cache($url_http . 'piwik.js')) {
268
      // A dummy query-string is added to filenames, to gain control over
269
      // browser-caching. The string changes on every update or full cache
270
      // flush, forcing browsers to load a new copy of the files, as the
271
      // URL changed.
272
      $query_string = '?' . variable_get('css_js_query_string', '0');
273

    
274
      $script .= 'g.src="' . $url . $query_string . '";';
275
    }
276
    else {
277
      $script .= 'g.src=u+"piwik.js";';
278
    }
279

    
280
    $script .= 's.parentNode.insertBefore(g,s);';
281
    $script .= '})();';
282

    
283
    // Add tracker code to scope.
284
    drupal_add_js($script, array('scope' => $scope, 'type' => 'inline'));
285
  }
286
}
287

    
288
/**
289
 * Implements hook_field_extra_fields().
290
 */
291
function piwik_field_extra_fields() {
292
  $extra['user']['user']['form']['piwik'] = array(
293
    'label' => t('Piwik configuration'),
294
    'description' => t('Piwik module form element.'),
295
    'weight' => 3,
296
  );
297

    
298
  return $extra;
299
}
300

    
301
/**
302
 * Implement hook_form_FORM_ID_alter().
303
 *
304
 * Allow users to decide if tracking code will be added to pages or not.
305
 */
306
function piwik_form_user_profile_form_alter(&$form, &$form_state) {
307
  $account = $form['#user'];
308
  $category = $form['#user_category'];
309

    
310
  if ($category == 'account' && user_access('opt-in or out of tracking') && ($custom = variable_get('piwik_custom', 0)) != 0 && _piwik_visibility_roles($account)) {
311
    $form['piwik'] = array(
312
      '#type' => 'fieldset',
313
      '#title' => t('Piwik configuration'),
314
      '#weight' => 3,
315
      '#collapsible' => TRUE,
316
      '#tree' => TRUE
317
    );
318

    
319
    switch ($custom) {
320
      case 1:
321
        $description = t('Users are tracked by default, but you are able to opt out.');
322
        break;
323

    
324
      case 2:
325
        $description = t('Users are <em>not</em> tracked by default, but you are able to opt in.');
326
        break;
327
    }
328

    
329
    $form['piwik']['custom'] = array(
330
      '#type' => 'checkbox',
331
      '#title' => t('Enable user tracking'),
332
      '#description' => $description,
333
      '#default_value' => isset($account->data['piwik']['custom']) ? $account->data['piwik']['custom'] : ($custom == 1),
334
    );
335

    
336
    return $form;
337
  }
338
}
339

    
340
/**
341
 * Implements hook_user_presave().
342
 */
343
function piwik_user_presave(&$edit, $account, $category) {
344
  if (isset($edit['piwik']['custom'])) {
345
    $edit['data']['piwik']['custom'] = $edit['piwik']['custom'];
346
  }
347
}
348

    
349
/**
350
 * Implements hook_cron().
351
 */
352
function piwik_cron() {
353
  // Regenerate the piwik.js every day.
354
  if (REQUEST_TIME - variable_get('piwik_last_cache', 0) >= 86400 && variable_get('piwik_cache', 0)) {
355
    _piwik_cache(variable_get('piwik_url_http', '') . 'piwik.js', TRUE);
356
    variable_set('piwik_last_cache', REQUEST_TIME);
357
  }
358
}
359

    
360
/**
361
 * Implements hook_preprocess_search_results().
362
 *
363
 * Collects and adds the number of search results to the head.
364
 */
365
function piwik_preprocess_search_results(&$variables) {
366
  // There is no search result $variable available that hold the number of items
367
  // found. But the pager item mumber can tell the number of search results.
368
  global $pager_total_items;
369

    
370
  drupal_add_js('window.piwik_search_results = ' . intval($pager_total_items[0]) . ';', array('type' => 'inline', 'group' => JS_LIBRARY-1));
371
}
372

    
373
/**
374
 * Download/Synchronize/Cache tracking code file locally.
375
 *
376
 * @param $location
377
 *   The full URL to the external javascript file.
378
 * @param $sync_cached_file
379
 *   Synchronize tracking code and update if remote file have changed.
380
 * @return mixed
381
 *   The path to the local javascript file on success, boolean FALSE on failure.
382
 */
383
function _piwik_cache($location, $sync_cached_file = FALSE) {
384
  $path = 'public://piwik';
385
  $file_destination = $path . '/' . basename($location);
386

    
387
  if (!file_exists($file_destination) || $sync_cached_file) {
388
    // Download the latest tracking code.
389
    $result = drupal_http_request($location);
390

    
391
    if ($result->code == 200) {
392
      if (file_exists($file_destination)) {
393
        // Synchronize tracking code and and replace local file if outdated.
394
        $data_hash_local = drupal_hash_base64(file_get_contents($file_destination));
395
        $data_hash_remote = drupal_hash_base64($result->data);
396
        // Check that the files directory is writable.
397
        if ($data_hash_local != $data_hash_remote && file_prepare_directory($path)) {
398
          // Save updated tracking code file to disk.
399
          file_unmanaged_save_data($result->data, $file_destination, FILE_EXISTS_REPLACE);
400
          watchdog('piwik', 'Locally cached tracking code file has been updated.', array(), WATCHDOG_INFO);
401

    
402
          // Change query-strings on css/js files to enforce reload for all users.
403
          _drupal_flush_css_js();
404
        }
405
      }
406
      else {
407
        // Check that the files directory is writable.
408
        if (file_prepare_directory($path, FILE_CREATE_DIRECTORY)) {
409
          // There is no need to flush JS here as core refreshes JS caches
410
          // automatically, if new files are added.
411
          file_unmanaged_save_data($result->data, $file_destination, FILE_EXISTS_REPLACE);
412
          watchdog('piwik', 'Locally cached tracking code file has been saved.', array(), WATCHDOG_INFO);
413

    
414
          // Return the local JS file path.
415
          return file_create_url($file_destination);
416
        }
417
      }
418
    }
419
  }
420
  else {
421
    // Return the local JS file path.
422
    return file_create_url($file_destination);
423
  }
424
}
425

    
426
/**
427
 * Delete cached files and directory.
428
 */
429
function piwik_clear_js_cache() {
430
  $path = 'public://piwik';
431
  if (file_prepare_directory($path)) {
432
    file_scan_directory($path, '/.*/', array('callback' => 'file_unmanaged_delete'));
433
    drupal_rmdir($path);
434

    
435
    // Change query-strings on css/js files to enforce reload for all users.
436
    _drupal_flush_css_js();
437

    
438
    watchdog('piwik', 'Local cache has been purged.', array(), WATCHDOG_INFO);
439
  }
440
}
441

    
442
/**
443
 * Helper function for grabbing search keys. Function is missing in D7.
444
 *
445
 * http://api.drupal.org/api/function/search_get_keys/6
446
 */
447
function piwik_search_get_keys() {
448
  static $return;
449
  if (!isset($return)) {
450
    // Extract keys as remainder of path
451
    // Note: support old GET format of searches for existing links.
452
    $path = explode('/', $_GET['q'], 3);
453
    $keys = empty($_REQUEST['keys']) ? '' : $_REQUEST['keys'];
454
    $return = count($path) == 3 ? $path[2] : $keys;
455
  }
456
  return $return;
457
}
458

    
459
/**
460
 * Tracking visibility check for an user object.
461
 *
462
 * @param $account
463
 *   A user object containing an array of roles to check.
464
 * @return boolean
465
 *   A decision on if the current user is being tracked by Piwik.
466
 */
467
function _piwik_visibility_user($account) {
468

    
469
  $enabled = FALSE;
470

    
471
  // Is current user a member of a role that should be tracked?
472
  if (_piwik_visibility_roles($account)) {
473

    
474
    // Use the user's block visibility setting, if necessary.
475
    if (($custom = variable_get('piwik_custom', 0)) != 0) {
476
      if ($account->uid && isset($account->data['piwik']['custom'])) {
477
        $enabled = $account->data['piwik']['custom'];
478
      }
479
      else {
480
        $enabled = ($custom == 1);
481
      }
482
    }
483
    else {
484
      $enabled = TRUE;
485
    }
486

    
487
  }
488

    
489
  return $enabled;
490
}
491

    
492
/**
493
 * Based on visibility setting this function returns TRUE if GA code should
494
 * be added for the current role and otherwise FALSE.
495
 */
496
function _piwik_visibility_roles($account) {
497

    
498
  $visibility = variable_get('piwik_visibility_roles', 0);
499
  $enabled = $visibility;
500
  $roles = variable_get('piwik_roles', array());
501

    
502
  if (array_sum($roles) > 0) {
503
    // One or more roles are selected.
504
    foreach (array_keys($account->roles) as $rid) {
505
      // Is the current user a member of one of these roles?
506
      if (isset($roles[$rid]) && $rid == $roles[$rid]) {
507
        // Current user is a member of a role that should be tracked/excluded from tracking.
508
        $enabled = !$visibility;
509
        break;
510
      }
511
    }
512
  }
513
  else {
514
    // No role is selected for tracking, therefore all roles should be tracked.
515
    $enabled = TRUE;
516
  }
517

    
518
  return $enabled;
519
}
520

    
521
/**
522
 * Based on visibility setting this function returns TRUE if GA code should
523
 * be added to the current page and otherwise FALSE.
524
 */
525
function _piwik_visibility_pages() {
526
  static $page_match;
527

    
528
  // Cache visibility setting in hook_init for hook_footer.
529
  if (!isset($page_match)) {
530

    
531
    $visibility = variable_get('piwik_visibility_pages', 0);
532
    $setting_pages = variable_get('piwik_pages', '');
533

    
534
    // Match path if necessary.
535
    if (!empty($setting_pages)) {
536
      // Convert path to lowercase. This allows comparison of the same path
537
      // with different case. Ex: /Page, /page, /PAGE.
538
      $pages = drupal_strtolower($setting_pages);
539
      if ($visibility < 2) {
540
        // Convert the Drupal path to lowercase
541
        $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
542
        // Compare the lowercase internal and lowercase path alias (if any).
543
        $page_match = drupal_match_path($path, $pages);
544
        if ($path != $_GET['q']) {
545
          $page_match = $page_match || drupal_match_path($_GET['q'], $pages);
546
        }
547
        // When $visibility has a value of 0, the tracking code is displayed on
548
        // all pages except those listed in $pages. When set to 1, it
549
        // is displayed only on those pages listed in $pages.
550
        $page_match = !($visibility xor $page_match);
551
      }
552
      elseif (module_exists('php')) {
553
        $page_match = php_eval($setting_pages);
554
      }
555
      else {
556
        $page_match = FALSE;
557
      }
558
    }
559
    else {
560
      $page_match = TRUE;
561
    }
562

    
563
  }
564
  return $page_match;
565
}
566

    
567
/**
568
 * Get the page titles trail for the current page.
569
 *
570
 * Based on menu_get_active_breadcrumb().
571
 *
572
 * @return array
573
 *   All page titles, including current page.
574
 */
575
function _piwik_get_hierarchy_titles() {
576
  $titles = array();
577

    
578
  // No breadcrumb for the front page.
579
  if (drupal_is_front_page()) {
580
    return $titles;
581
  }
582

    
583
  $item = menu_get_item();
584
  if (!empty($item['access'])) {
585
    $active_trail = menu_get_active_trail();
586

    
587
    // Allow modules to alter the breadcrumb, if possible, as that is much
588
    // faster than rebuilding an entirely new active trail.
589
    drupal_alter('menu_breadcrumb', $active_trail, $item);
590

    
591
    // Remove the tab root (parent) if the current path links to its parent.
592
    // Normally, the tab root link is included in the breadcrumb, as soon as we
593
    // are on a local task or any other child link. However, if we are on a
594
    // default local task (e.g., node/%/view), then we do not want the tab root
595
    // link (e.g., node/%) to appear, as it would be identical to the current
596
    // page. Since this behavior also needs to work recursively (i.e., on
597
    // default local tasks of default local tasks), and since the last non-task
598
    // link in the trail is used as page title (see menu_get_active_title()),
599
    // this condition cannot be cleanly integrated into menu_get_active_trail().
600
    // menu_get_active_trail() already skips all links that link to their parent
601
    // (commonly MENU_DEFAULT_LOCAL_TASK). In order to also hide the parent link
602
    // itself, we always remove the last link in the trail, if the current
603
    // router item links to its parent.
604
    if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
605
      array_pop($active_trail);
606
    }
607

    
608
    foreach ($active_trail as $parent) {
609
      $titles[] = $parent['title'];
610
    }
611
  }
612

    
613
  return $titles;
614
}