features.menu.inc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <?php
  2. /**
  3. * Implements hook_features_api().
  4. */
  5. function menu_features_api() {
  6. return array(
  7. 'menu_custom' => array(
  8. 'name' => t('Menus'),
  9. 'default_hook' => 'menu_default_menu_custom',
  10. 'feature_source' => TRUE,
  11. 'default_file' => FEATURES_DEFAULTS_INCLUDED,
  12. ),
  13. 'menu_links' => array(
  14. 'name' => t('Menu links'),
  15. 'default_hook' => 'menu_default_menu_links',
  16. 'feature_source' => TRUE,
  17. 'default_file' => FEATURES_DEFAULTS_INCLUDED,
  18. ),
  19. // DEPRECATED
  20. 'menu' => array(
  21. 'name' => t('Menu items'),
  22. 'default_hook' => 'menu_default_items',
  23. 'default_file' => FEATURES_DEFAULTS_INCLUDED,
  24. 'feature_source' => FALSE,
  25. ),
  26. );
  27. }
  28. /**
  29. * Implements hook_features_export().
  30. * DEPRECATED: This implementation simply migrates deprecated `menu` items
  31. * to the `menu_links` type.
  32. */
  33. function menu_features_export($data, &$export, $module_name = '') {
  34. $pipe = array();
  35. foreach ($data as $path) {
  36. $pipe['menu_links'][] = "features:{$path}";
  37. }
  38. return $pipe;
  39. }
  40. /**
  41. * Implements hook_features_export_options().
  42. */
  43. function menu_custom_features_export_options() {
  44. $options = array();
  45. $result = db_query("SELECT * FROM {menu_custom} ORDER BY title", array(), array('fetch' => PDO::FETCH_ASSOC));
  46. foreach ($result as $menu) {
  47. $options[$menu['menu_name']] = $menu['title'];
  48. }
  49. return $options;
  50. }
  51. /**
  52. * Implements hook_features_export().
  53. */
  54. function menu_custom_features_export($data, &$export, $module_name = '') {
  55. // Default hooks are provided by the feature module so we need to add
  56. // it as a dependency.
  57. $export['dependencies']['features'] = 'features';
  58. $export['dependencies']['menu'] = 'menu';
  59. // Collect a menu to module map
  60. $pipe = array();
  61. $map = features_get_default_map('menu_custom', 'menu_name');
  62. foreach ($data as $menu_name) {
  63. // If this menu is provided by a different module, add it as a dependency.
  64. if (isset($map[$menu_name]) && $map[$menu_name] != $module_name) {
  65. $export['dependencies'][$map[$menu_name]] = $map[$menu_name];
  66. }
  67. else {
  68. $export['features']['menu_custom'][$menu_name] = $menu_name;
  69. }
  70. }
  71. return $pipe;
  72. }
  73. /**
  74. * Implements hook_features_export_render()
  75. */
  76. function menu_custom_features_export_render($module, $data) {
  77. $code = array();
  78. $code[] = ' $menus = array();';
  79. $code[] = '';
  80. $translatables = array();
  81. foreach ($data as $menu_name) {
  82. $row = db_select('menu_custom')
  83. ->fields('menu_custom')
  84. ->condition('menu_name', $menu_name)
  85. ->execute()
  86. ->fetchAssoc();
  87. if ($row) {
  88. $export = features_var_export($row, ' ');
  89. $code[] = " // Exported menu: {$menu_name}.";
  90. $code[] = " \$menus['{$menu_name}'] = {$export};";
  91. $translatables[] = $row['title'];
  92. $translatables[] = $row['description'];
  93. }
  94. }
  95. if (!empty($translatables)) {
  96. $code[] = features_translatables_export($translatables, ' ');
  97. }
  98. $code[] = '';
  99. $code[] = ' return $menus;';
  100. $code = implode("\n", $code);
  101. return array('menu_default_menu_custom' => $code);
  102. }
  103. /**
  104. * Implements hook_features_revert().
  105. */
  106. function menu_custom_features_revert($module) {
  107. menu_custom_features_rebuild($module);
  108. }
  109. /**
  110. * Implements hook_features_rebuild().
  111. */
  112. function menu_custom_features_rebuild($module) {
  113. if ($defaults = features_get_default('menu_custom', $module)) {
  114. foreach ($defaults as $menu) {
  115. menu_save($menu);
  116. }
  117. }
  118. }
  119. /**
  120. * Implements hook_features_export_options().
  121. */
  122. function menu_links_features_export_options() {
  123. global $menu_admin;
  124. // Need to set this to TRUE in order to get menu links that the
  125. // current user may not have access to (i.e. user/login)
  126. $menu_admin = TRUE;
  127. $use_menus = array_intersect_key(menu_get_menus(), array_flip(array_filter(variable_get('features_admin_menu_links_menus', array_keys(menu_get_menus())))));
  128. $menu_links = menu_parent_options($use_menus, array('mlid' => 0));
  129. $options = array();
  130. foreach ($menu_links as $key => $name) {
  131. list($menu_name, $mlid) = explode(':', $key, 2);
  132. if ($mlid != 0) {
  133. $link = menu_link_load($mlid);
  134. $identifier = menu_links_features_identifier($link, TRUE);
  135. $options[$identifier] = "{$menu_name}: {$name}";
  136. }
  137. }
  138. $menu_admin = FALSE;
  139. return $options;
  140. }
  141. /**
  142. * Callback for generating the menu link exportable identifier.
  143. */
  144. function menu_links_features_identifier($link, $old = FALSE) {
  145. // Add some uniqueness to these identifiers, allowing multiple links with the same path, but different titles.
  146. $clean_title = features_clean_title(isset($link['title']) ? $link['title'] : $link['link_title']);
  147. // The old identifier is requested.
  148. if ($old) {
  149. // if identifier already exists
  150. if (isset($link['options']['identifier'])) {
  151. return $link['options']['identifier'];
  152. }
  153. // providing backward compatibility and allowing/enabling multiple links with same paths
  154. else {
  155. $identifier = isset($link['menu_name'], $link['link_path']) ? "{$link['menu_name']}:{$link['link_path']}" : FALSE;
  156. // Checking if there are multiples of this identifier
  157. if (features_menu_link_load($identifier) !== FALSE) {
  158. // this is where we return the upgrade posibility for links.
  159. return $identifier;
  160. }
  161. }
  162. }
  163. return isset($link['menu_name'], $link['link_path']) ? "{$link['menu_name']}_{$clean_title}:{$link['link_path']}" : FALSE;
  164. }
  165. /**
  166. * Implements hook_features_export().
  167. */
  168. function menu_links_features_export($data, &$export, $module_name = '') {
  169. // Default hooks are provided by the feature module so we need to add
  170. // it as a dependency.
  171. $export['dependencies']['features'] = 'features';
  172. $export['dependencies']['menu'] = 'menu';
  173. // Collect a link to module map
  174. $pipe = array();
  175. $map = features_get_default_map('menu_links', 'menu_links_features_identifier');
  176. foreach ($data as $key => $identifier) {
  177. if ($link = features_menu_link_load($identifier)) {
  178. // If this link is provided by a different module, add it as a dependency.
  179. $new_identifier = menu_links_features_identifier($link, empty($export));
  180. if (isset($map[$identifier]) && $map[$identifier] != $module_name) {
  181. $export['dependencies'][$map[$identifier]] = $map[$identifier];
  182. }
  183. else {
  184. $export['features']['menu_links'][$new_identifier] = $new_identifier;
  185. }
  186. // For now, exclude a variety of common menus from automatic export.
  187. // They may still be explicitly included in a Feature if the builder
  188. // chooses to do so.
  189. if (!in_array($link['menu_name'], array('features', 'primary-links', 'secondary-links', 'navigation', 'admin', 'devel'))) {
  190. $pipe['menu_custom'][] = $link['menu_name'];
  191. }
  192. }
  193. }
  194. return $pipe;
  195. }
  196. /**
  197. * Implements hook_features_export_render()
  198. */
  199. function menu_links_features_export_render($module, $data, $export = NULL) {
  200. $code = array();
  201. $code[] = ' $menu_links = array();';
  202. $code[] = '';
  203. $translatables = array();
  204. foreach ($data as $identifier) {
  205. if ($link = features_menu_link_load($identifier)) {
  206. $new_identifier = menu_links_features_identifier($link, empty($export));
  207. // Replace plid with a parent path.
  208. if (!empty($link['plid']) && $parent = menu_link_load($link['plid'])) {
  209. // If the new identifier is different than the old, maintain
  210. // 'parent_path' for backwards compatibility.
  211. if ($new_identifier != menu_links_features_identifier($link)) {
  212. $link['parent_path'] = $parent['link_path'];
  213. }
  214. else {
  215. $clean_title = features_clean_title($parent['title']);
  216. $link['parent_identifier'] = "{$parent['menu_name']}_{$clean_title}:{$parent['link_path']}";
  217. }
  218. }
  219. if (isset($export)) {
  220. // Don't show new identifier unless we are actually exporting.
  221. $link['options']['identifier'] = $new_identifier;
  222. // identifiers are renewed, => that means we need to update them in the db
  223. menu_link_save($temp = $link);
  224. }
  225. unset($link['plid']);
  226. unset($link['mlid']);
  227. $code[] = " // Exported menu link: {$new_identifier}";
  228. $code[] = " \$menu_links['{$new_identifier}'] = ". features_var_export($link, ' ') .";";
  229. $translatables[] = $link['link_title'];
  230. }
  231. }
  232. if (!empty($translatables)) {
  233. $code[] = features_translatables_export($translatables, ' ');
  234. }
  235. $code[] = '';
  236. $code[] = ' return $menu_links;';
  237. $code = implode("\n", $code);
  238. return array('menu_default_menu_links' => $code);
  239. }
  240. /**
  241. * Implements hook_features_revert().
  242. */
  243. function menu_links_features_revert($module) {
  244. menu_links_features_rebuild($module);
  245. }
  246. /**
  247. * Implements hook_features_rebuild().
  248. */
  249. function menu_links_features_rebuild($module) {
  250. if ($menu_links = features_get_default('menu_links', $module)) {
  251. menu_links_features_rebuild_ordered($menu_links);
  252. }
  253. }
  254. /**
  255. * Generate a depth tree of all menu links.
  256. */
  257. function menu_links_features_rebuild_ordered($menu_links, $reset = FALSE) {
  258. static $ordered;
  259. static $all_links;
  260. if (!isset($ordered) || $reset) {
  261. $ordered = array();
  262. $unordered = features_get_default('menu_links');
  263. // Order all links by depth.
  264. if ($unordered) {
  265. do {
  266. $current = count($unordered);
  267. foreach ($unordered as $key => $link) {
  268. $identifier = menu_links_features_identifier($link);
  269. $parent = isset($link['parent_identifier']) ? $link['parent_identifier'] : '';
  270. if (empty($parent)) {
  271. $ordered[$identifier] = 0;
  272. $all_links[$identifier] = $link;
  273. unset($unordered[$key]);
  274. }
  275. elseif (isset($ordered[$parent])) {
  276. $ordered[$identifier] = $ordered[$parent] + 1;
  277. $all_links[$identifier] = $link;
  278. unset($unordered[$key]);
  279. }
  280. }
  281. } while (count($unordered) < $current);
  282. }
  283. asort($ordered);
  284. }
  285. // Ensure any default menu items that do not exist are created.
  286. foreach (array_keys($ordered) as $identifier) {
  287. $link = $all_links[$identifier];
  288. $existing = features_menu_link_load($identifier);
  289. if (!$existing || in_array($link, $menu_links)) {
  290. // Retrieve the mlid if this is an existing item.
  291. if ($existing) {
  292. $link['mlid'] = $existing['mlid'];
  293. }
  294. // Retrieve the plid for a parent link.
  295. if (!empty($link['parent_identifier']) && $parent = features_menu_link_load($link['parent_identifier'])) {
  296. $link['plid'] = $parent['mlid'];
  297. }
  298. // This if for backwards compatibility.
  299. elseif (!empty($link['parent_path']) && $parent = features_menu_link_load("{$link['menu_name']}:{$link['parent_path']}")) {
  300. $link['plid'] = $parent['mlid'];
  301. }
  302. else {
  303. $link['plid'] = 0;
  304. }
  305. menu_link_save($link);
  306. }
  307. }
  308. }
  309. /**
  310. * Load a menu link by its menu_name_cleantitle:link_path identifier.
  311. * Also matches links with unique menu_name:link_path
  312. */
  313. function features_menu_link_load($identifier) {
  314. $menu_name = '';
  315. $link_path = '';
  316. // This gets variables for menu_name_cleantitle:link_path format.
  317. if (strstr($identifier, "_")) {
  318. $link_path = substr($identifier, strpos($identifier, ":") + 1);
  319. list($menu_name) = explode('_', $identifier, 2);
  320. $clean_title = substr($identifier, strpos($identifier, "_") + 1, strpos($identifier, ":") - strpos($identifier, "_") - 1);
  321. }
  322. // This gets variables for traditional identifier format.
  323. else {
  324. $clean_title = '';
  325. list($menu_name, $link_path) = explode(':', $identifier, 2);
  326. }
  327. $links = db_select('menu_links')
  328. ->fields('menu_links', array('menu_name', 'mlid', 'plid', 'link_path', 'router_path', 'link_title', 'options', 'module', 'hidden', 'external', 'has_children', 'expanded', 'weight', 'customized'))
  329. ->condition('menu_name', $menu_name)
  330. ->condition('link_path', $link_path)
  331. ->execute()
  332. ->fetchAllAssoc('mlid');
  333. foreach($links as $link) {
  334. $link->options = unserialize($link->options);
  335. // Title or previous identifier matches.
  336. if ((isset($link->options['identifier']) && strcmp($link->options['identifier'], $identifier) == 0)
  337. || (isset($clean_title) && strcmp(features_clean_title($link->link_title), $clean_title) == 0)) {
  338. return (array)$link;
  339. }
  340. }
  341. // Only one link with the requested menu_name and link_path does exists,
  342. // -- providing an upgrade possibility for links saved in a feature before the
  343. // new identifier-pattern was added.
  344. if (count($links) == 1 && empty($clean_title)) {
  345. $link = reset($links); // get the first item
  346. return (array)$link;
  347. }
  348. // If link_path was changed on an existing link, we need to find it by
  349. // searching for link_title.
  350. else if (isset($clean_title)) {
  351. $links = db_select('menu_links')
  352. ->fields('menu_links', array('menu_name', 'mlid', 'plid', 'link_path', 'router_path', 'link_title', 'options', 'module', 'hidden', 'external', 'has_children', 'expanded', 'weight'))
  353. ->condition('menu_name', $menu_name)
  354. ->execute()
  355. ->fetchAllAssoc('mlid');
  356. foreach($links as $link) {
  357. $link->options = unserialize($link->options);
  358. // title or previous identifier matches
  359. if ((isset($link->options['identifier']) && strcmp($link->options['identifier'], $identifier) == 0)
  360. || (isset($clean_title) && strcmp(features_clean_title($link->link_title), $clean_title) == 0)) {
  361. return (array)$link;
  362. }
  363. }
  364. }
  365. return FALSE;
  366. }
  367. /**
  368. * Returns a lowercase clean string with only letters, numbers and dashes
  369. */
  370. function features_clean_title($str) {
  371. return strtolower(preg_replace_callback('/(\s)|([^a-zA-Z\-0-9])/i', create_function(
  372. '$matches',
  373. 'return $matches[1]?"-":"";'
  374. ), $str));
  375. }