PageRenderTime 66ms CodeModel.GetById 35ms RepoModel.GetById 1ms app.codeStats 0ms

/app/controllers/components/acl_menu/menu.php

https://github.com/ata/steak
PHP | 508 lines | 270 code | 31 blank | 207 comment | 44 complexity | fbdb302b23bb4aefb83f7da8913a15ff MD5 | raw file
  1. <?php
  2. /**
  3. * Menu Component
  4. *
  5. * Uses ACL to generate Menus.
  6. *
  7. * Copyright 2008, Mark Story.
  8. *
  9. * Licensed under The MIT License
  10. * Redistributions of files must retain the above copyright notice.
  11. *
  12. * @copyright Copyright 2008, Mark Story.
  13. * @link http://mark-story.com
  14. * @version 1.1
  15. * @author Mark Story <mark@mark-story.com>
  16. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  17. */
  18. class MenuComponent extends Object {
  19. /**
  20. * The Default Menu Parent for things that have no parent element defined
  21. * used a lot by menu items generated by controller folder scrapings
  22. *
  23. * @var string
  24. */
  25. public $defaultMenuParent = null;
  26. /**
  27. * Set to false to disable the auto menu generation in startup()
  28. * Useful if you want your menus generated off of Aro's other than the user in the current session.
  29. *
  30. * @var boolean
  31. */
  32. public $autoLoad = true;
  33. /**
  34. * Controller reference
  35. *
  36. * @var object
  37. */
  38. public $Controller = null;
  39. /**
  40. * Components used by Menu
  41. *
  42. * @var array
  43. */
  44. public $components = array('Acl', 'Auth');
  45. /**
  46. * Key for the caching
  47. *
  48. * @var string
  49. */
  50. public $cacheKey = 'menu_storage';
  51. /**
  52. * Time to cache menus for.
  53. *
  54. * @var string String compatible with strtotime.
  55. */
  56. public $cacheTime = '+1 day';
  57. /**
  58. * cache config key
  59. *
  60. * @var string
  61. */
  62. public $cacheConfig = 'menu_component';
  63. /**
  64. * Separator between controller and action name.
  65. *
  66. * @var string
  67. */
  68. public $aclSeparator = '/';
  69. /**
  70. * The Node path to get to the controller listing
  71. *
  72. * @var string
  73. **/
  74. public $aclPath = 'controllers/';
  75. /**
  76. * Array of Actions to exclude when making menus.
  77. * Per controller exclusions can be set with Controller::menuOptions
  78. *
  79. * @var array
  80. **/
  81. public $excludeActions = array('view', 'edit', 'delete', 'admin_edit', 'admin_delete', 'admin_edit', 'admin_view');
  82. /**
  83. * Completed list of methods to not include in menus. Includes all of Controller's methods.
  84. *
  85. * @var array
  86. **/
  87. public $excludedMethods = array();
  88. /**
  89. * The Completed menu for the current user.
  90. *
  91. * @var array
  92. */
  93. public $menu = array();
  94. /**
  95. * Raw menus before formatting, either loaded from parsing controllers directory or loading Cache
  96. *
  97. * @var array
  98. */
  99. public $rawMenus = array();
  100. /**
  101. * Internal Flag to check if new menus have been added to a cached menu set. Indicates that new menu items
  102. * have been added and that menus need to be rebuilt.
  103. *
  104. */
  105. protected $_rebuildMenus = false;
  106. /**
  107. * initialize function
  108. *
  109. * Takes Settings declared in Controller and assigns them.
  110. *
  111. * @return bool
  112. **/
  113. public function initialize(&$Controller, $settings) {
  114. if (!empty($settings)) {
  115. $this->_set($settings);
  116. }
  117. return true;
  118. }
  119. /**
  120. * Startup Method
  121. *
  122. * Automatically makes menus for all a the controllers based on the current user.
  123. * If $this->autoLoad = false then you must manually loadCache(),
  124. * contstructMenu() and writeCache().
  125. *
  126. * @param Object $Controller
  127. */
  128. public function startup(&$Controller) {
  129. $this->Controller =& $Controller;
  130. Cache::config($this->cacheConfig, array('engine' => 'File', 'duration' => $this->cacheTime, 'prefix' => $this->cacheKey));
  131. //no active session, no menu can be generated
  132. if (!$this->Auth->user()) {
  133. return;
  134. }
  135. if ($this->autoLoad) {
  136. $this->loadCache();
  137. $this->constructMenu($this->Auth->user());
  138. $this->writeCache();
  139. }
  140. }
  141. /**
  142. * Write the current Block Access data to a file.
  143. *
  144. * @return boolean on success of writing a file.
  145. */
  146. public function writeCache() {
  147. $data = array(
  148. 'menus' => $this->rawMenus
  149. );
  150. if (Cache::write($this->cacheKey, $data, $this->cacheConfig)) {
  151. return true;
  152. }
  153. $this->log('Menu Component - Could not write Menu cache.');
  154. return false;
  155. }
  156. /**
  157. * Load the Cached Permissions and restore them
  158. *
  159. * @return boolean true if cache was loaded.
  160. */
  161. public function loadCache() {
  162. if ($data = Cache::read($this->cacheKey, $this->cacheConfig)) {
  163. $this->rawMenus = $this->_mergeMenuCache($data['menus']);
  164. return true;
  165. }
  166. $this->_rebuildMenus = true;
  167. return false;
  168. }
  169. /**
  170. * Clears the raw Menu Cache, this will in turn force
  171. * a menu rebuild for each ARO that needs a menu.
  172. *
  173. * @return boolean
  174. **/
  175. public function clearCache() {
  176. return Cache::delete($this->cacheKey, $this->cacheConfig);
  177. }
  178. /**
  179. * Construct the menus From the Controllers in the Application. This is an expensive
  180. * Process Timewise and is cached.
  181. *
  182. * @param string $aro Aro Alias / identification array that a menu is needed for.
  183. */
  184. public function constructMenu($aro) {
  185. $aroKey = $aro;
  186. if (is_array($aro)) {
  187. $aroKey = key($aro) . $aro[key($aro)]['id'];
  188. }
  189. $cacheKey = $aroKey . '_' . $this->cacheKey;
  190. $completeMenu = Cache::read($cacheKey, $this->cacheConfig);
  191. if (!$completeMenu || $this->_rebuildMenus == true) {
  192. $this->generateRawMenus();
  193. $menu = array();
  194. $size = count($this->rawMenus);
  195. for ($i = 0; $i < $size; $i++) {
  196. $item = $this->rawMenus[$i];
  197. $aco = Inflector::underscore($item['url']['controller']);
  198. if (isset($item['url']['action'])) {
  199. $aco = $this->aclPath . $aco . $this->aclSeparator . $item['url']['action'];
  200. }
  201. if ($this->Acl->check($aro, $aco)) {
  202. if (!isset($menu[$item['id']])) {
  203. $menu[$item['id']] = $this->rawMenus[$i];
  204. }
  205. }
  206. }
  207. $completeMenu = $this->_formatMenu($menu);
  208. Cache::write($cacheKey, $completeMenu, $this->cacheConfig);
  209. }
  210. $this->menu = $completeMenu;
  211. }
  212. /**
  213. * Generate Raw Menus from Controller in the Application
  214. * Loads a list of All controllers in the app/controllers, imports the class and gets a method
  215. * list. Uses a common exclusion list to remove unwanted methods. Each Controller can specify a
  216. * menuOptions var which allows additional menu configuration.
  217. *
  218. * Menu Options for Controllers:
  219. * exclude => actions to exclude from the menu list
  220. * parent => Parent link to add a controller / actions underneath
  221. * alias => array of action => aliases Allows you to set friendly link names for actions
  222. *
  223. * @return void sets $this->rawMenus
  224. */
  225. public function generateRawMenus() {
  226. $Controllers = $this->getControllers();
  227. $cakeAdmin = Configure::read('Routing.admin');
  228. $this->createExclusions();
  229. //go through the controllers folder and make an array of every menu that could be used.
  230. foreach($Controllers as $Controller) {
  231. if ($Controller == 'App') {
  232. continue;
  233. }
  234. $ctrlName = $Controller;
  235. App::import('Controller', $ctrlName);
  236. $ctrlclass = $ctrlName . 'Controller';
  237. $methods = get_class_methods($ctrlclass);
  238. $classVars = get_class_vars($ctrlclass);
  239. $menuOptions = $this->setOptions($classVars);
  240. if ($menuOptions === false) {
  241. continue;
  242. }
  243. $methods = $this->filterMethods($methods, $menuOptions['exclude']);
  244. $ctrlCamel = Inflector::variable($ctrlName);
  245. $ctrlHuman = Inflector::humanize(Inflector::underscore($ctrlCamel));
  246. $methodList = array();
  247. $adminController = false;
  248. foreach ($methods as $action) {
  249. $camelAction = Inflector::variable($action);
  250. if (empty($menuOptions['alias']) || !isset($menuOptions['alias'][$action])) {
  251. $human = Inflector::humanize(Inflector::underscore($action));
  252. } else {
  253. $human = $menuOptions['alias'][$action];
  254. }
  255. $url = array(
  256. 'controller' => $ctrlCamel,
  257. 'action' => $action
  258. );
  259. if ($cakeAdmin) {
  260. $url[$cakeAdmin] = false;
  261. }
  262. if (strpos($action, $cakeAdmin . '_') !== false && $cakeAdmin) {
  263. $url[$cakeAdmin] = true;
  264. $adminController = true;
  265. }
  266. $parent = $menuOptions['controllerButton'] ? $ctrlCamel : $menuOptions['parent'];
  267. $this->rawMenus[] = array(
  268. 'parent' => $parent,
  269. 'id' => $this->_createId($ctrlCamel, $action),
  270. 'title' => $human,
  271. 'url' => $url,
  272. 'weight' => 0,
  273. );
  274. }
  275. if ($menuOptions['controllerButton']) {
  276. //If an admin index exists use it.
  277. $action = $adminController ? $cakeAdmin . '_index' : 'index';
  278. $url = array(
  279. 'controller' => $ctrlCamel,
  280. 'action' => $action,
  281. 'admin' => $adminController,
  282. );
  283. $menuItem = array(
  284. 'parent' => $menuOptions['parent'],
  285. 'id' => $ctrlCamel,
  286. 'title' => $ctrlHuman,
  287. 'url' => $url,
  288. 'weight' => 0
  289. );
  290. $this->rawMenus[] = $menuItem;
  291. }
  292. }
  293. }
  294. /**
  295. * Get the Controllers in the Application
  296. *
  297. * @access public
  298. * @return void
  299. */
  300. public function getControllers() {
  301. return Configure::listObjects('controller');
  302. }
  303. /**
  304. * filter out methods based on $menuOptions.
  305. * Removes private actions as well.
  306. *
  307. * @param array $methods Array of methods to prepare
  308. * @param array $remove Array of additional Methods to remove, normally options on the controller.
  309. * @return array
  310. **/
  311. public function filterMethods($methods, $remove = array()) {
  312. if (!empty($remove)) {
  313. $remove = array_map('strtolower', $remove);
  314. }
  315. $exclusions = array_merge($this->excludedMethods, $remove);
  316. foreach ($methods as $k => $method) {
  317. $method = strtolower($method);
  318. if (strpos($method, '_', 0) === 0) {
  319. unset($methods[$k]);
  320. }
  321. if (in_array($method, $exclusions)) {
  322. unset($methods[$k]);
  323. }
  324. }
  325. return array_values($methods);
  326. }
  327. /**
  328. * Set the Options for the current Controller.
  329. *
  330. * @return mixed. Array of options or false on total exclusion
  331. **/
  332. public function setOptions($controllerVars) {
  333. $cakeAdmin = Configure::read('Routing.admin');
  334. $menuOptions = isset($controllerVars['menuOptions']) ? $controllerVars['menuOptions'] : array();
  335. $exclude = array('view', 'edit', 'delete', $cakeAdmin . '_edit',
  336. $cakeAdmin . '_delete', $cakeAdmin . '_edit', $cakeAdmin . '_view');
  337. $defaults = array(
  338. 'exclude' => $exclude,
  339. 'alias' => array(),
  340. 'parent' => $this->defaultMenuParent,
  341. 'controllerButton' => true
  342. );
  343. $menuOptions = Set::merge($defaults, $menuOptions);
  344. if (in_array('*', (array)$menuOptions['exclude'])) {
  345. return false;
  346. }
  347. return $menuOptions;
  348. }
  349. /**
  350. * Creates the Exclusions for generating menus.
  351. *
  352. * @return void
  353. **/
  354. public function createExclusions() {
  355. $methods = array_merge(get_class_methods('Controller'), $this->excludeActions);
  356. $this->excludedMethods = array_map('strtolower', $methods);
  357. }
  358. /**
  359. * Add a Menu Item.
  360. * Allows manual Insertion into the menu system.
  361. * If Added after constructMenu() It will not be shown
  362. *
  363. * @param string $parent
  364. * @param array $menu
  365. * 'Menu' Array
  366. * 'title' => name
  367. * 'url' => url array of menu, url strings are lame and won't work
  368. * 'key' => unique name of this menu for parenting purposes.
  369. * 'controller' => controller Name this action is from
  370. */
  371. public function addMenu($menu) {
  372. $defaults = array(
  373. 'title' => null,
  374. 'url' => null,
  375. 'parent' => null,
  376. 'id' => null,
  377. 'weight' => 0,
  378. );
  379. $menu = array_merge($defaults, $menu);
  380. if (!$menu['id'] && isset($menu['url'])) {
  381. $menu['id'] = $this->_createId($menu['url']);
  382. }
  383. if (!$menu['title'] && isset($menu['url']['action'])) {
  384. $menu['title'] = Inflector::humanize($menu['url']['action']);
  385. }
  386. $this->rawMenus[] = $menu;
  387. }
  388. /**
  389. * BeforeRender Callback.
  390. *
  391. */
  392. public function beforeRender() {
  393. $this->Controller->set('menu', $this->menu);
  394. }
  395. /**
  396. * Make a Unique Menu item key
  397. *
  398. * @param array $parts
  399. * @return string Unique key name
  400. */
  401. protected function _createId() {
  402. $parts = func_get_args();
  403. if (is_array($parts[0])) {
  404. $parts = $parts[0];
  405. }
  406. $key = Inflector::variable(implode('-', $parts));
  407. return $key;
  408. }
  409. /**
  410. * Recursive function to construct Menu
  411. *
  412. * @param unknown_type $menu
  413. * @param unknown_type $parentId
  414. */
  415. protected function _formatMenu($menu) {
  416. $out = $idMap = array();
  417. foreach ($menu as $item) {
  418. $item['children'] = array();
  419. $id = $item['id'];
  420. $parentId = $item['parent'];
  421. if (isset($idMap[$id]['children'])) {
  422. $idMap[$id] = am($item, $idMap[$id]);
  423. } else {
  424. $idMap[$id] = am($item, array('children' => array()));
  425. }
  426. if ($parentId) {
  427. $idMap[$parentId]['children'][] =& $idMap[$id];
  428. } else {
  429. $out[] =& $idMap[$id];
  430. }
  431. }
  432. usort($out, array(&$this, '_sortMenu'));
  433. return $out;
  434. }
  435. /**
  436. * Sort the menu before returning it. Used with usort()
  437. *
  438. * @return int
  439. **/
  440. protected function _sortMenu($one, $two) {
  441. if ($one['weight'] == $two['weight']) {
  442. return 1;
  443. }
  444. return ($one['weight'] < $two['weight']) ? -1 : 1;
  445. }
  446. /**
  447. * Merge the Cached menus with the Menus added in Controller::beforeFilter to ensure they are unique.
  448. *
  449. * @param array $cachedMenus
  450. * @return array Merged Menus
  451. */
  452. protected function _mergeMenuCache($cachedMenus) {
  453. $cacheCount = sizeOf($cachedMenus);
  454. $currentCount = sizeOf($this->rawMenus);
  455. $tmp = array();
  456. for ($i = 0; $i < $currentCount; $i++) {
  457. $exist = false;
  458. $addedMenu = $this->rawMenus[$i];
  459. for ($j =0; $j < $cacheCount; $j++) {
  460. $cachedItem = $cachedMenus[$j];
  461. if ($addedMenu['id'] == $cachedItem['id']) {
  462. $exist = true;
  463. break;
  464. }
  465. }
  466. if ($exist) {
  467. continue;
  468. }
  469. $tmp[] = $addedMenu;
  470. }
  471. if (!empty($tmp)) {
  472. $this->_rebuildMenus = true;
  473. }
  474. return array_merge($cachedMenus, $tmp);
  475. }
  476. }
  477. ?>