features_override.export.inc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <?php
  2. /**
  3. * @file
  4. * Helper function to export features overrides.
  5. */
  6. /**
  7. * Parses the identifier into indivudal parts.
  8. *
  9. * As the keys may have a period in them, cannot use explode or similair ways.
  10. *
  11. * @param $identifier
  12. * A string in the form <comonent>.<element>.<keys> or <component>.<element>.
  13. * @return
  14. * An array of component, element, and keys string
  15. * @see features_override_make_key()
  16. */
  17. function features_override_parse_identifier($identifier) {
  18. $first_period = strpos($identifier, '.');
  19. $component = substr($identifier, 0, $first_period);
  20. if ($second_period = strpos($identifier, '.', $first_period + 1)) {
  21. $element = substr($identifier, $first_period + 1, $second_period - $first_period - 1);
  22. $keys = substr($identifier, $second_period + 1);
  23. }
  24. else {
  25. $element = substr($identifier, $first_period + 1);
  26. $keys = FALSE;
  27. }
  28. return array($component, $element, $keys);
  29. }
  30. /**
  31. * Makes a distinct string key from an array of keys.
  32. *
  33. * @param $keys
  34. * An array of keys.
  35. * @return
  36. * A string representation of the keys.
  37. */
  38. function features_override_make_key($keys) {
  39. if (is_array($keys)) {
  40. $return_keys = array();
  41. foreach ($keys as $key) {
  42. $return_keys[] = $key['key'];
  43. }
  44. return implode('|', $return_keys);
  45. }
  46. else {
  47. return $keys;
  48. }
  49. }
  50. /**
  51. * Returns an array of keys to be ignored for various exportables
  52. * @param $component
  53. * The component to retrieve ignore_keys from.
  54. */
  55. function features_get_ignore_keys($component) {
  56. static $cache;
  57. if (!isset($cache[$component])) {
  58. $cache[$component] = module_invoke_all('features_override_ignore', $component);
  59. }
  60. return $cache[$component];
  61. }
  62. /**
  63. * Calculautes what overrides exist for by component/element.
  64. *
  65. * @param $component_key
  66. * A component key that's defined via hook_features_api.
  67. * @param $element_key
  68. * A key identifieing an element that's been overriden.
  69. * @param $reset
  70. * Reset the internal cache of overrides gathered.
  71. * @param $all
  72. * If TRUE, return all overrides, otherwise only overrides not yet in an override feature
  73. * */
  74. function features_override_get_overrides($component_key = FALSE, $element_key = FALSE, $reset = FALSE, $all = TRUE) {
  75. static $cache;
  76. if (!isset($cache) || $reset) {
  77. $cache = array();
  78. module_load_include('inc', 'features', 'features.export');
  79. features_include();
  80. foreach (features_get_components() as $component => $info) {
  81. if (empty($info['default_hook']) || $component == 'features_override_items' || $component == 'features_overrides' || !features_get_default_alter_hook($component) | !features_hook($component, 'features_export_render')) {
  82. continue;
  83. }
  84. features_include_defaults($component);
  85. foreach (module_implements($info['default_hook']) as $module) {
  86. if ($differences = array_filter(features_override_module_component_overrides($module, $component, $reset, $all))) {
  87. $cache[$component] = isset($cache[$component]) ? array_merge($differences, $cache[$component]) : $differences;
  88. }
  89. }
  90. $cache[$component] = isset($cache[$component]) ? array_filter($cache[$component]) : array();
  91. }
  92. }
  93. if ($component_key && $element_key) {
  94. return !empty($cache[$component_key][$element_key]) ? $cache[$component_key][$element_key] : array();
  95. }
  96. elseif ($component_key) {
  97. return !empty($cache[$component_key]) ? $cache[$component_key] : array();
  98. }
  99. return $cache;
  100. }
  101. /**
  102. * Get overrides for specific module/component.
  103. *
  104. * @param $module
  105. * An enabled module to find overrides for it's components.
  106. * @param $component
  107. * A type of component to find overrides for.
  108. * @param $reset
  109. * Reset the internal cache of overrides gathered.
  110. * @param $all
  111. * If TRUE, return all overrides, otherwise only overrides not yet in an override feature
  112. * @return
  113. * An array of overrides found.
  114. */
  115. function features_override_module_component_overrides($module, $component, $reset = FALSE, $all = TRUE) {
  116. static $cache = array();
  117. if (isset($cache[$module][$component]) && !$reset) {
  118. return $cache[$module][$component];
  119. }
  120. module_load_include('inc', 'features_override', 'features_override.hooks');
  121. features_include();
  122. features_include_defaults($component);
  123. // Allows overriding non-feature controlled code.
  124. $default_hook = features_get_default_hooks($component);
  125. if ($all) {
  126. // call hooks directly
  127. // could also do
  128. // $default = features_get_default($component, $module, FALSE, $reset);
  129. // but this is more efficient
  130. $default = module_invoke($module, $default_hook);
  131. }
  132. else {
  133. $default = features_get_default($component, $module, TRUE, $reset);
  134. }
  135. $normal = features_get_normal($component, $module, $reset);
  136. // This indicates it is likely not controlled by features, so fetch manually.
  137. if (!$normal && is_array($default)) {
  138. $code = array_pop(features_invoke($component, 'features_export_render', $module, array_keys($default), NULL));
  139. if (!$code) {
  140. return FALSE;
  141. }
  142. else {
  143. $normal = eval($code);
  144. }
  145. }
  146. $context = array(
  147. 'component' => $component,
  148. 'module' => $module,
  149. );
  150. // Can't use _features_sanitize as that resets some keys.
  151. _features_override_sanitize($normal);
  152. _features_override_sanitize($default);
  153. // make a deep copy of data to prevent problems when removing recursion later
  154. $default_copy = unserialize(serialize($default));
  155. $normal_copy = unserialize(serialize($normal));
  156. $ignore_keys = features_get_ignore_keys($component);
  157. // remove keys to be ignored
  158. // doing this now allows us to better control which recursive parts are removed
  159. if (count($ignore_keys)) {
  160. _features_override_remove_ignores($default_copy, $ignore_keys);
  161. _features_override_remove_ignores($normal_copy, $ignore_keys);
  162. }
  163. // now remove any remaining recursion
  164. features_override_remove_recursion($default_copy);
  165. features_override_remove_recursion($normal_copy);
  166. $component_overrides = array();
  167. if ($normal && is_array($normal) || is_object($normal)) {
  168. foreach ($normal as $name => $properties) {
  169. $component_overrides[$name] = array(
  170. 'additions' => array(),
  171. 'deletions' => array(),
  172. );
  173. if (isset($default_copy[$name])) {
  174. drupal_alter('features_override_component_overrides', $default_copy[$name], $normal_copy[$name], $context);
  175. _features_override_set_additions($default_copy[$name], $normal_copy[$name], $component_overrides[$name]['additions'], $ignore_keys);
  176. _features_override_set_deletions($default_copy[$name], $normal_copy[$name], $component_overrides[$name]['deletions'], $ignore_keys);
  177. }
  178. if (!array_filter($component_overrides[$name])) {
  179. $component_overrides[$name] = FALSE;
  180. }
  181. }
  182. // now check for any elements that are in $default but not in $normal that we didn't process yet
  183. foreach ($default as $name => $properties) {
  184. if (!isset($normal_copy[$name])) {
  185. $_keys = array(array('type' => 'array', 'key' => $name));
  186. $component_overrides[$name]['deletions'][features_override_make_key($name)] = array(
  187. 'keys' => $name,
  188. );
  189. }
  190. }
  191. }
  192. $cache[$module][$component] = $component_overrides;
  193. return $component_overrides;
  194. }
  195. /**
  196. * Sorts an array by its keys (assoc) or values (non-assoc).
  197. *
  198. * @param $array
  199. * An array that needs to be sorted.
  200. */
  201. function _features_override_sanitize(&$array) {
  202. if (is_array($array)) {
  203. $is_assoc = (array_keys($array) !== range(0, count($array) - 1));
  204. if ($is_assoc) {
  205. ksort($array);
  206. }
  207. else {
  208. sort($array);
  209. }
  210. foreach ($array as $k => $v) {
  211. if (is_array($v)) {
  212. _features_override_sanitize($array[$k]);
  213. }
  214. }
  215. }
  216. }
  217. /**
  218. * Helper function to set the additions between default and normal features.
  219. *
  220. * @param $default
  221. * The default defination of a component.
  222. * @param $normal
  223. * The current defination of a component.
  224. * @param $additions
  225. * An array of currently gathered additions.
  226. * @param $ignore_keys
  227. * Keys to ignore while processing element.
  228. * @param $level
  229. * How many levels deep into object.
  230. * @param $keys
  231. * The keys for this level.
  232. */
  233. function _features_override_set_additions(&$default, &$normal, &$additions, $ignore_keys = array(), $level = 0, $keys = array()) {
  234. $object = is_object($normal);
  235. foreach ($normal as $key => $value) {
  236. if (isset($ignore_keys[$key]) && ($level == $ignore_keys[$key])) {
  237. continue;
  238. }
  239. if ($object) {
  240. if (!is_object($default) || !property_exists($default, $key) || (is_scalar($value) && ($default->$key !== $value))) {
  241. $_keys = array_merge($keys, array(array('type' => 'object', 'key' => $key)));
  242. $additions[features_override_make_key($_keys)] = array(
  243. 'keys' => $_keys,
  244. 'value' => $value,
  245. 'original' => (is_scalar($value) && isset($default->$key)) ? $default->$key : '',
  246. );
  247. }
  248. elseif (property_exists($default, $key) && ($default->$key !== $value)) {
  249. _features_override_set_additions($default->$key, $value, $additions, $ignore_keys, $level+1, array_merge($keys, array(array('type' => 'object', 'key' => $key))));
  250. }
  251. }
  252. elseif (is_array($normal)) {
  253. if (!is_array($default) || !array_key_exists($key, $default) || (is_scalar($value) && ($default[$key] !== $value))) {
  254. $_keys = array_merge($keys, array(array('type' => 'array', 'key' => $key)));
  255. $additions[features_override_make_key($_keys)] = array(
  256. 'keys' => $_keys,
  257. 'value' => $value,
  258. 'original' => (is_scalar($value) && isset($default[$key])) ? $default[$key] : '',
  259. );
  260. }
  261. elseif (array_key_exists($key, $default) && (!is_null($value) && ($default[$key] !== $value))) {
  262. _features_override_set_additions($default[$key], $value, $additions, $ignore_keys, $level+1, array_merge($keys, array(array('type' => 'array', 'key' => $key))));
  263. }
  264. }
  265. }
  266. }
  267. /**
  268. * Helper function to set the deletions between default and normal features.
  269. *
  270. * @param $default
  271. * The default defination of a component.
  272. * @param $normal
  273. * The current defination of a component.
  274. * @param $deletions
  275. * An array of currently gathered deletions.
  276. * @param $ignore_keys
  277. * Keys to ignore while processing element.
  278. * @param $level
  279. * How many levels deep into object.
  280. * @param $keys
  281. * The keys for this level.
  282. */
  283. function _features_override_set_deletions(&$default, &$normal, &$deletions, $ignore_keys = array(), $level = 0, $keys = array()) {
  284. $object = is_object($default);
  285. foreach ($default as $key => $value) {
  286. if (isset($ignore_keys[$key]) && ($level == $ignore_keys[$key])) {
  287. continue;
  288. }
  289. if ($object) {
  290. if (!property_exists($normal, $key)) {
  291. $_keys = array_merge($keys, array(array('type' => 'object', 'key' => $key)));
  292. $deletions[features_override_make_key($_keys)] = array(
  293. 'keys' => $_keys,
  294. );
  295. }
  296. elseif (property_exists($normal, $key) && (is_array($value) || is_object($value))) {
  297. _features_override_set_deletions($value, $normal->$key, $deletions, $ignore_keys, $level+1, array_merge($keys, array(array('type' => 'object', 'key' => $key))));
  298. }
  299. }
  300. else {
  301. if (!array_key_exists($key, $normal)) {
  302. $_keys = array_merge($keys, array(array('type' => 'array', 'key' => $key)));
  303. $deletions[features_override_make_key($_keys)] = array(
  304. 'keys' => $_keys,
  305. );
  306. }
  307. elseif (array_key_exists($key, $normal) && (is_array($value) || is_object($value))) {
  308. _features_override_set_deletions($value, $normal[$key], $deletions, $ignore_keys, $level+1, array_merge($keys, array(array('type' => 'array', 'key' => $key))));
  309. }
  310. }
  311. }
  312. }
  313. /**
  314. * Creates a string representation of an array of keys.
  315. *
  316. * @param $keys
  317. * An array of keys with their associate types.
  318. *
  319. * @return
  320. * A string representation of the keys.
  321. */
  322. function features_override_export_keys($keys) {
  323. $line = '';
  324. if (is_array($keys)) {
  325. foreach ($keys as $key) {
  326. if ($key['type'] == 'object') {
  327. $line .= "->{$key['key']}";
  328. }
  329. else {
  330. $line .= "['{$key['key']}']";
  331. }
  332. }
  333. }
  334. return $line;
  335. }
  336. /**
  337. * Removes recursion from an object or array.
  338. *
  339. * @param $item
  340. * An object or array passed by reference.
  341. */
  342. function features_override_remove_recursion(&$item) {
  343. _features_override_remove_recursion($item);
  344. _features_override_remove_recursion_markers($item);
  345. }
  346. /**
  347. * Helper to removes recursion from an object/array.
  348. *
  349. * @param $item
  350. * An object or array passed by reference.
  351. */
  352. function _features_override_remove_recursion(&$item) {
  353. $is_object = is_object($item);
  354. if ($is_object) {
  355. $item->{FEATURES_OVERRIDE_RECURSION_MARKER} = 1;
  356. }
  357. else {
  358. $item[FEATURES_OVERRIDE_RECURSION_MARKER] = 1;
  359. }
  360. foreach ($item as $key => $value) {
  361. if (is_array($value) || is_object($value)) {
  362. $remove = is_array($value) ? !empty($value[FEATURES_OVERRIDE_RECURSION_MARKER]) : !empty($value->{FEATURES_OVERRIDE_RECURSION_MARKER});
  363. if ($remove) {
  364. if ($is_object) {
  365. unset($item->$key);
  366. }
  367. else {
  368. unset($item[$key]);
  369. }
  370. }
  371. else {
  372. features_override_remove_recursion($value);
  373. }
  374. }
  375. }
  376. }
  377. /**
  378. * Helper to removes recursion from an object/array.
  379. *
  380. * @param $item
  381. * An object or array passed by reference.
  382. */
  383. function _features_override_remove_recursion_markers(&$item) {
  384. $is_object = is_object($item);
  385. foreach ($item as $key => $value) {
  386. if ($key === FEATURES_OVERRIDE_RECURSION_MARKER) {
  387. if ($is_object) {
  388. unset($item->$key);
  389. }
  390. else {
  391. unset($item[$key]);
  392. }
  393. }
  394. elseif (is_array($value) || is_object($value)) {
  395. _features_override_remove_recursion_markers($value);
  396. }
  397. }
  398. }
  399. /**
  400. * Helper to removes a set of keys an object/array.
  401. *
  402. * @param $item
  403. * An object or array passed by reference.
  404. * @param $ignore_keys
  405. * Array of keys to be ignored. Values are the level of the key.
  406. * @param $level
  407. * Level of key to remove. Up to 2 levels deep because $item can still be
  408. * recursive
  409. */
  410. function _features_override_remove_ignores(&$item, $ignore_keys, $level = -1) {
  411. $is_object = is_object($item);
  412. foreach ($item as $key => $value) {
  413. if (isset($ignore_keys[$key]) && ($ignore_keys[$key] == $level)) {
  414. if ($is_object) {
  415. unset($item->$key);
  416. }
  417. else {
  418. unset($item[$key]);
  419. }
  420. }
  421. elseif (($level < 2) && (is_array($value) || is_object($value))) {
  422. _features_override_remove_ignores($value, $ignore_keys, $level+1);
  423. }
  424. }
  425. }
  426. /**
  427. * Drupal-friendly var_export().
  428. *
  429. * @param $var
  430. * The variable to export.
  431. * @param $prefix
  432. * A prefix that will be added at the beginning of every lines of the output.
  433. * @return
  434. * The variable exported in a way compatible to Drupal's coding standards.
  435. */
  436. function features_override_var_export($var, $prefix = '') {
  437. if (is_array($var) || is_object($var)) {
  438. // Special causing array so calls features_override_var_export instead of
  439. // features_var_export.
  440. if (is_array($var)) {
  441. if (empty($var)) {
  442. $output = 'array()';
  443. }
  444. else {
  445. $output = "array(\n";
  446. foreach ($var as $key => $value) {
  447. // Using normal var_export on the key to ensure correct quoting.
  448. $output .= " " . var_export($key, TRUE) . " => " . features_override_var_export($value, ' ', FALSE) . ",\n";
  449. }
  450. $output .= ')';
  451. }
  452. }
  453. // Objects do not export cleanily.
  454. else {
  455. if (method_exists($var, 'export')) {
  456. $output = $var->export();
  457. }
  458. elseif (get_class($var) === 'stdClass') {
  459. $output = '(object) ' . features_override_var_export((array) $var, $prefix);
  460. }
  461. elseif (!method_exists($var, '__set_state')) {
  462. // Ugly, but custom object with no clue how to export.without
  463. // __set_state class and var_export produces unusable code.
  464. $output = 'unserialize(' . var_export(serialize($var), TRUE) . ')';
  465. }
  466. else {
  467. $output = var_export($var, TRUE);
  468. }
  469. }
  470. }
  471. else {
  472. module_load_include('inc', 'features', 'features.export');
  473. $output = features_var_export($var);
  474. }
  475. if ($prefix) {
  476. $output = str_replace("\n", "\n$prefix", $output);
  477. }
  478. return $output;
  479. }
  480. /**
  481. * Renders the addition/change to an element.
  482. */
  483. function features_override_features_export_render_addition($alter, $element, $component, $is_change = TRUE) {
  484. module_load_include('inc', 'features_override', 'features_override.hooks');
  485. if (features_hook($component, 'features_override_export_render_addition')) {
  486. return features_invoke($component, 'features_override_export_render_addition', $alter, $element);
  487. }
  488. else {
  489. $code = array();
  490. $component_start = "\$data['$element']";
  491. $code_line = features_override_export_keys($alter['keys']);
  492. $value_export = features_override_var_export($alter['value'], ' ');
  493. if ($is_change) {
  494. $original_export = (isset($alter['original'])) ? ' /* WAS: ' . features_override_var_export($alter['original'], ' ') . ' */' : '';
  495. }
  496. else {
  497. $original_export = '';
  498. }
  499. $code[] = " " . $component_start . $code_line . ' = ' . $value_export . ';' . $original_export;
  500. return $code;
  501. }
  502. }
  503. /**
  504. * Renders the deletion to an element.
  505. */
  506. function features_override_features_export_render_deletion($alter, $element, $component) {
  507. module_load_include('inc', 'features_override', 'features_override.hooks');
  508. if (features_hook($component, 'features_override_export_render_deletion')) {
  509. return features_invoke($component, 'features_override_export_render_deletion', $alter, $element);
  510. }
  511. else {
  512. $code = array();
  513. $component_start = "\$data['$element']";
  514. $code_line = features_override_export_keys($alter['keys']);
  515. $code[] = ' unset(' . $component_start . $code_line . ');';
  516. return $code;
  517. }
  518. }
  519. /**
  520. * Encodes a string for use as option.
  521. * @see features_dom_encode_options()
  522. * @param $string
  523. * A string to encode.
  524. * @return
  525. * An encoded string for use as option value.
  526. */
  527. function features_override_encode_string($string) {
  528. $replacements = array(
  529. ':' => '__'. ord(':') .'__',
  530. '/' => '__'. ord('/') .'__',
  531. ',' => '__'. ord(',') .'__',
  532. '.' => '__'. ord('.') .'__',
  533. '<' => '__'. ord('<') .'__',
  534. '>' => '__'. ord('>') .'__',
  535. );
  536. return strtr($string, $replacements);
  537. }