PageRenderTime 65ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/admin-menu-editor/includes/menu.php

https://gitlab.com/mattswann/launch-housing
PHP | 483 lines | 304 code | 57 blank | 122 comment | 44 complexity | 03c145f3eeeba15114226b6239eb8355 MD5 | raw file
  1. <?php
  2. abstract class ameMenu {
  3. const format_name = 'Admin Menu Editor menu';
  4. const format_version = '6.4';
  5. /**
  6. * Load an admin menu from a JSON string.
  7. *
  8. * @static
  9. *
  10. * @param string $json A JSON-encoded menu structure.
  11. * @param bool $assume_correct_format Skip the format header check and assume everything is fine. Defaults to false.
  12. * @param bool $always_normalize Always normalize the menu structure, even if format[is_normalized] is true.
  13. * @throws InvalidMenuException
  14. * @return array
  15. */
  16. public static function load_json($json, $assume_correct_format = false, $always_normalize = false) {
  17. $arr = json_decode($json, true);
  18. if ( !is_array($arr) ) {
  19. throw new InvalidMenuException('The input is not a valid JSON-encoded admin menu.');
  20. }
  21. return self::load_array($arr, $assume_correct_format, $always_normalize);
  22. }
  23. /**
  24. * Load an admin menu structure from an associative array.
  25. *
  26. * @static
  27. *
  28. * @param array $arr
  29. * @param bool $assume_correct_format
  30. * @param bool $always_normalize
  31. * @throws InvalidMenuException
  32. * @return array
  33. */
  34. public static function load_array($arr, $assume_correct_format = false, $always_normalize = false){
  35. $is_normalized = false;
  36. if ( !$assume_correct_format ) {
  37. if ( isset($arr['format']) && ($arr['format']['name'] == self::format_name) ) {
  38. $compared = version_compare($arr['format']['version'], self::format_version);
  39. if ( $compared > 0 ) {
  40. throw new InvalidMenuException(sprintf(
  41. "Can't load a menu created by a newer version of the plugin. Menu format: '%s', newest supported format: '%s'.",
  42. $arr['format']['version'],
  43. self::format_version
  44. ));
  45. }
  46. //We can skip normalization if the version number matches exactly and the menu is already normalized.
  47. if ( ($compared === 0) && isset($arr['format']['is_normalized']) ) {
  48. $is_normalized = $arr['format']['is_normalized'];
  49. }
  50. } else {
  51. return self::load_menu_40($arr);
  52. }
  53. }
  54. if ( !(isset($arr['tree']) && is_array($arr['tree'])) ) {
  55. throw new InvalidMenuException("Failed to load a menu - the menu tree is missing.");
  56. }
  57. if ( isset($arr['format']) && !empty($arr['format']['compressed']) ) {
  58. $arr = self::decompress($arr);
  59. }
  60. $menu = array('tree' => array());
  61. $menu = self::add_format_header($menu);
  62. if ( $is_normalized && !$always_normalize ) {
  63. $menu['tree'] = $arr['tree'];
  64. } else {
  65. foreach($arr['tree'] as $file => $item) {
  66. $menu['tree'][$file] = ameMenuItem::normalize($item);
  67. }
  68. $menu['format']['is_normalized'] = true;
  69. }
  70. if ( isset($arr['color_css']) && is_string($arr['color_css']) ) {
  71. $menu['color_css'] = $arr['color_css'];
  72. $menu['color_css_modified'] = isset($arr['color_css_modified']) ? intval($arr['color_css_modified']) : 0;
  73. }
  74. //Sanitize color presets.
  75. if ( isset($arr['color_presets']) && is_array($arr['color_presets']) ) {
  76. $color_presets = array();
  77. foreach($arr['color_presets'] as $name => $preset) {
  78. $name = substr(trim(strip_tags(strval($name))), 0, 250);
  79. if ( empty($name) || !is_array($preset) ) {
  80. continue;
  81. }
  82. //Each color must be a hexadecimal HTML color code. For example: "#12456"
  83. $is_valid_preset = true;
  84. foreach($preset as $property => $color) {
  85. //Note: It would good to check $property against a list of known color names.
  86. if ( !is_string($property) || !is_string($color) || !preg_match('/^\#[0-9a-f]{6}$/i', $color) ) {
  87. $is_valid_preset = false;
  88. break;
  89. }
  90. }
  91. if ( $is_valid_preset ) {
  92. $color_presets[$name] = $preset;
  93. }
  94. }
  95. $menu['color_presets'] = $color_presets;
  96. }
  97. //Copy directly granted capabilities.
  98. if ( isset($arr['granted_capabilities']) && is_array($arr['granted_capabilities']) ) {
  99. $granted_capabilities = array();
  100. foreach($arr['granted_capabilities'] as $actor => $capabilities) {
  101. //Skip empty lists to avoid problems with {} => [] and to save space.
  102. if ( !empty($capabilities) ) {
  103. $granted_capabilities[strval($actor)] = $capabilities;
  104. }
  105. }
  106. if (!empty($granted_capabilities)) {
  107. $menu['granted_capabilities'] = $granted_capabilities;
  108. }
  109. }
  110. return $menu;
  111. }
  112. /**
  113. * "Pre-load" an old menu structure.
  114. *
  115. * In older versions of the plugin, the entire menu consisted of
  116. * just the menu tree and nothing else. This was internally known as
  117. * menu format "4".
  118. *
  119. * To improve portability and forward-compatibility, newer versions
  120. * use a simple dictionary-based container instead, with the menu tree
  121. * being one of the possible entries.
  122. *
  123. * @static
  124. * @param array $arr
  125. * @return array
  126. */
  127. private static function load_menu_40($arr) {
  128. //This is *very* basic and might need to be improved.
  129. $menu = array('tree' => $arr);
  130. return self::load_array($menu, true);
  131. }
  132. private static function add_format_header($menu) {
  133. if ( !isset($menu['format']) || !is_array($menu['format']) ) {
  134. $menu['format'] = array();
  135. }
  136. $menu['format'] = array_merge(
  137. $menu['format'],
  138. array(
  139. 'name' => self::format_name,
  140. 'version' => self::format_version,
  141. )
  142. );
  143. return $menu;
  144. }
  145. /**
  146. * Serialize an admin menu as JSON.
  147. *
  148. * @static
  149. * @param array $menu
  150. * @return string
  151. */
  152. public static function to_json($menu) {
  153. $menu = self::add_format_header($menu);
  154. return json_encode($menu);
  155. }
  156. /**
  157. * Sort the menus and menu items of a given menu according to their positions
  158. *
  159. * @param array $tree A menu structure in the internal format (just the tree).
  160. * @return array Sorted menu in the internal format
  161. */
  162. public static function sort_menu_tree($tree){
  163. //Resort the tree to ensure the found items are in the right spots
  164. uasort($tree, 'ameMenuItem::compare_position');
  165. //Resort all submenus as well
  166. foreach ($tree as &$topmenu){
  167. if (!empty($topmenu['items'])){
  168. usort($topmenu['items'], 'ameMenuItem::compare_position');
  169. }
  170. }
  171. return $tree;
  172. }
  173. /**
  174. * Convert the WP menu structure to the internal representation. All properties set as defaults.
  175. *
  176. * @param array $menu
  177. * @param array $submenu
  178. * @param array $blacklist
  179. * @return array Menu in the internal tree format.
  180. */
  181. public static function wp2tree($menu, $submenu, $blacklist = array()){
  182. $tree = array();
  183. foreach ($menu as $pos => $item){
  184. $tree_item = ameMenuItem::blank_menu();
  185. $tree_item['defaults'] = ameMenuItem::fromWpItem($item, $pos);
  186. $tree_item['separator'] = $tree_item['defaults']['separator'];
  187. //Attach sub-menu items
  188. $parent = $tree_item['defaults']['file'];
  189. if ( isset($submenu[$parent]) ){
  190. foreach($submenu[$parent] as $position => $subitem){
  191. $defaults = ameMenuItem::fromWpItem($subitem, $position, $parent);
  192. //Skip blacklisted items.
  193. if ( isset($defaults['url'], $blacklist[$defaults['url']]) ) {
  194. continue;
  195. }
  196. $tree_item['items'][] = array_merge(
  197. ameMenuItem::blank_menu(),
  198. array('defaults' => $defaults)
  199. );
  200. }
  201. }
  202. //Skip blacklisted top level menus (only if they have no submenus).
  203. if (
  204. empty($tree_item['items'])
  205. && isset($tree_item['defaults']['url'], $blacklist[$tree_item['defaults']['url']])
  206. ) {
  207. continue;
  208. }
  209. $tree[$parent] = $tree_item;
  210. }
  211. $tree = self::sort_menu_tree($tree);
  212. return $tree;
  213. }
  214. /**
  215. * Check if a menu contains any items with the "hidden" flag set to true.
  216. *
  217. * @param array $menu
  218. * @return bool
  219. */
  220. public static function has_hidden_items($menu) {
  221. if ( !is_array($menu) || empty($menu) || empty($menu['tree']) ) {
  222. return false;
  223. }
  224. foreach($menu['tree'] as $item) {
  225. if ( ameMenuItem::get($item, 'hidden') ) {
  226. return true;
  227. }
  228. if ( !empty($item['items']) ) {
  229. foreach($item['items'] as $child) {
  230. if ( ameMenuItem::get($child, 'hidden') ) {
  231. return true;
  232. }
  233. }
  234. }
  235. }
  236. return false;
  237. }
  238. /**
  239. * Sanitize a list of menu items. Array indexes will be preserved.
  240. *
  241. * @param array $treeItems A list of menu items.
  242. * @param bool $unfiltered_html Whether the current user has the unfiltered_html capability.
  243. * @return array List of sanitized items.
  244. */
  245. public static function sanitize($treeItems, $unfiltered_html = null) {
  246. if ( $unfiltered_html === null ) {
  247. $unfiltered_html = current_user_can('unfiltered_html');
  248. }
  249. $result = array();
  250. foreach($treeItems as $key => $item) {
  251. $item = ameMenuItem::sanitize($item, $unfiltered_html);
  252. if ( !empty($item['items']) ) {
  253. $item['items'] = self::sanitize($item['items'], $unfiltered_html);
  254. }
  255. $result[$key] = $item;
  256. }
  257. return $result;
  258. }
  259. /**
  260. * Recursively filter a list of menu items and remove items flagged as missing.
  261. *
  262. * @param array $items An array of menu items to filter.
  263. * @return array
  264. */
  265. public static function remove_missing_items($items) {
  266. $items = array_filter($items, array(__CLASS__, 'is_not_missing'));
  267. foreach($items as &$item) {
  268. if ( !empty($item['items']) ) {
  269. $item['items'] = self::remove_missing_items($item['items']);
  270. }
  271. }
  272. return $items;
  273. }
  274. protected static function is_not_missing($item) {
  275. return empty($item['missing']);
  276. }
  277. /**
  278. * Compress menu configuration (lossless).
  279. *
  280. * Reduces data size by storing commonly used properties and defaults in one place
  281. * instead of in every menu item.
  282. *
  283. * @param array $menu
  284. * @return array
  285. */
  286. public static function compress($menu) {
  287. $property_dict = ameMenuItem::blank_menu();
  288. unset($property_dict['defaults']);
  289. $common = array(
  290. 'properties' => $property_dict,
  291. 'basic_defaults' => ameMenuItem::basic_defaults(),
  292. 'custom_item_defaults' => ameMenuItem::custom_item_defaults(),
  293. );
  294. $menu['tree'] = self::map_items(
  295. $menu['tree'],
  296. array(__CLASS__, 'compress_item'),
  297. array($common)
  298. );
  299. $menu = self::add_format_header($menu);
  300. $menu['format']['compressed'] = true;
  301. $menu['format']['common'] = $common;
  302. return $menu;
  303. }
  304. protected static function compress_item($item, $common) {
  305. //These empty arrays can be dropped. They'll be restored either by merging common properties,
  306. //or by ameMenuItem::normalize().
  307. if ( empty($item['grant_access']) ) {
  308. unset($item['grant_access']);
  309. }
  310. if ( empty($item['items']) ) {
  311. unset($item['items']);
  312. }
  313. //Normal and custom menu items have different defaults.
  314. //Remove defaults that are the same for all items of that type.
  315. $defaults = !empty($item['custom']) ? $common['custom_item_defaults'] : $common['basic_defaults'];
  316. if ( isset($item['defaults']) ) {
  317. foreach($defaults as $key => $value) {
  318. if ( array_key_exists($key, $item['defaults']) && $item['defaults'][$key] === $value ) {
  319. unset($item['defaults'][$key]);
  320. }
  321. }
  322. }
  323. //Remove properties that match the common values.
  324. foreach($common['properties'] as $key => $value) {
  325. if ( array_key_exists($key, $item) && $item[$key] === $value ) {
  326. unset($item[$key]);
  327. }
  328. }
  329. return $item;
  330. }
  331. /**
  332. * Decompress menu configuration that was previously compressed by ameMenu::compress().
  333. *
  334. * If the input $menu is not compressed, this method will return it unchanged.
  335. *
  336. * @param array $menu
  337. * @return array
  338. */
  339. public static function decompress($menu) {
  340. if ( !isset($menu['format']) || empty($menu['format']['compressed']) ) {
  341. return $menu;
  342. }
  343. $common = $menu['format']['common'];
  344. $menu['tree'] = self::map_items(
  345. $menu['tree'],
  346. array(__CLASS__, 'decompress_item'),
  347. array($common)
  348. );
  349. unset($menu['format']['compressed'], $menu['format']['common']);
  350. return $menu;
  351. }
  352. protected static function decompress_item($item, $common) {
  353. $item = array_merge($common['properties'], $item);
  354. $defaults = !empty($item['custom']) ? $common['custom_item_defaults'] : $common['basic_defaults'];
  355. $item['defaults'] = array_merge($defaults, $item['defaults']);
  356. return $item;
  357. }
  358. /**
  359. * Recursively apply a callback to every menu item in an array and return the results.
  360. * Array keys are preserved.
  361. *
  362. * @param array $items
  363. * @param callable $callback
  364. * @param array|null $extra_params Optional. An array of additional parameters to pass to the callback.
  365. * @return array
  366. */
  367. protected static function map_items($items, $callback, $extra_params = null) {
  368. if ( $extra_params === null ) {
  369. $extra_params = array();
  370. }
  371. $result = array();
  372. foreach($items as $key => $item) {
  373. $args = array_merge(array($item), $extra_params);
  374. $item = call_user_func_array($callback, $args);
  375. if ( !empty($item['items']) ) {
  376. $item['items'] = self::map_items($item['items'], $callback, $extra_params);
  377. }
  378. $result[$key] = $item;
  379. }
  380. return $result;
  381. }
  382. }
  383. class ameGrantedCapabilityFilter {
  384. private $post_types = array();
  385. private $taxonomies = array();
  386. public function __construct() {
  387. $this->post_types = get_post_types(array('public' => true, 'show_ui' => true), 'names', 'or');
  388. $this->taxonomies = get_taxonomies(array('public' => true, 'show_ui' => true), 'names', 'or');
  389. }
  390. /**
  391. * Remove capabilities that refer to unregistered post types or taxonomies.
  392. *
  393. * @param array $granted_capabilities
  394. * @return array
  395. */
  396. public function clean_up($granted_capabilities) {
  397. $clean = array();
  398. foreach($granted_capabilities as $actor => $capabilities) {
  399. $clean[$actor] = array_filter($capabilities, array($this, 'is_registered_source'));
  400. }
  401. return $clean;
  402. }
  403. private function is_registered_source($grant) {
  404. if ( !is_array($grant) || !isset($grant[1]) ) {
  405. return true;
  406. }
  407. if ( isset($grant[2]) ) {
  408. if ( $grant[1] === 'post_type' ) {
  409. return array_key_exists($grant[2], $this->post_types);
  410. } else if ( $grant[1] === 'taxonomy' ) {
  411. return array_key_exists($grant[2], $this->taxonomies);
  412. }
  413. }
  414. return false;
  415. }
  416. }
  417. class InvalidMenuException extends Exception {}
  418. class ameInvalidJsonException extends RuntimeException {};