Completed
Push — master ( a9d66b...f95d57 )
by Hamish
26s
created

CMSMenu::remove_menu_class()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 3
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
3
use SilverStripe\Security\Member;
4
/**
5
 * The object manages the main CMS menu. See {@link LeftAndMain::init()} for
6
 * example usage.
7
 *
8
 * The menu will be automatically populated with menu items for subclasses of
9
 * {@link LeftAndMain}. That is, for each class in the CMS that creates an
10
 * administration panel, a CMS menu item will be created. The default
11
 * configuration will also include a 'help' link to the SilverStripe user
12
 * documentation.
13
 *
14
 * Additional CMSMenu items can be added through {@link LeftAndMainExtension::init()}
15
 * extensions added to {@link LeftAndMain}.
16
 *
17
 * @package framework
18
 * @subpackage admin
19
 */
20
class
21
CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider {
0 ignored issues
show
Coding Style introduced by
The extends keyword must be on the same line as the class name
Loading history...
Coding Style introduced by
The implements keyword must be on the same line as the class name
Loading history...
22
23
	/**
24
	 * Sort by menu priority, highest to lowest
25
	 */
26
	const MENU_PRIORITY = 'menu_priority';
27
28
	/**
29
	 * Sort by url priority, highest to lowest
30
	 */
31
	const URL_PRIORITY = 'url_priority';
32
33
	/**
34
	 * An array of changes to be made to the menu items, in the order that the changes should be
35
	 * applied.  Each item is a map in one of the two forms:
36
	 *  - array('type' => 'add', 'item' => new CMSMenuItem(...) )
37
	 *  - array('type' => 'remove', 'code' => 'codename' )
38
	 */
39
	protected static $menu_item_changes = array();
40
41
	/**
42
	 * Set to true if clear_menu() is called, to indicate that the default menu shouldn't be
43
	 * included
44
	 */
45
	protected static $menu_is_cleared = false;
46
47
	/**
48
	 * Generate CMS main menu items by collecting valid
49
	 * subclasses of {@link LeftAndMain}
50
	 */
51
	public static function populate_menu() {
52
		self::$menu_is_cleared = false;
53
	}
54
55
	/**
56
	 * Add a LeftAndMain controller to the CMS menu.
57
	 *
58
	 * @param string $controllerClass The class name of the controller
59
	 * @todo A director rule is added when a controller link is added, but it won't be removed
60
	 *			when the item is removed. Functionality needed in {@link Director}.
61
	 */
62
	public static function add_controller($controllerClass) {
63
		if($menuItem = self::menuitem_for_controller($controllerClass)) {
64
			self::add_menu_item_obj($controllerClass, $menuItem);
65
		}
66
	}
67
68
	/**
69
	 * Return a CMSMenuItem to add the given controller to the CMSMenu
70
	 *
71
	 * @param string $controllerClass
72
	 * @return CMSMenuItem
73
	 */
74
	protected static function menuitem_for_controller($controllerClass) {
75
		$urlBase = AdminRootController::admin_url();
76
		$urlSegment   = Config::inst()->get($controllerClass, 'url_segment', Config::FIRST_SET);
77
		$menuPriority = Config::inst()->get($controllerClass, 'menu_priority', Config::FIRST_SET);
78
79
		// Don't add menu items defined the old way
80
		if (!$urlSegment) {
81
			return null;
82
		}
83
84
		$link = Controller::join_links($urlBase, $urlSegment) . '/';
85
86
		// doesn't work if called outside of a controller context (e.g. in _config.php)
87
		// as the locale won't be detected properly. Use {@link LeftAndMain->MainMenu()} to update
88
		// titles for existing menu entries
89
		$menuTitle = LeftAndMain::menu_title($controllerClass);
90
91
		return new CMSMenuItem($menuTitle, $link, $controllerClass, $menuPriority);
92
	}
93
94
95
	/**
96
	 * Add an arbitrary URL to the CMS menu.
97
	 *
98
	 * @param string $code A unique identifier (used to create a CSS ID and its key in {@link $menu_items})
99
	 * @param string $menuTitle The link's title in the CMS menu
100
	 * @param string $url The url of the link
101
	 * @param integer $priority The menu priority (sorting order) of the menu item.  Higher priorities will be further
102
	 *                          left.
103
	 * @param array $attributes an array of attributes to include on the link.
104
	 *
105
	 * @return boolean The result of the operation.
106
	 */
107
	public static function add_link($code, $menuTitle, $url, $priority = -1, $attributes = null) {
108
		return self::add_menu_item($code, $menuTitle, $url, null, $priority, $attributes);
109
	}
110
111
	/**
112
	 * Add a navigation item to the main administration menu showing in the top bar.
113
	 *
114
	 * uses {@link CMSMenu::$menu_items}
115
	 *
116
	 * @param string $code Unique identifier for this menu item (e.g. used by {@link replace_menu_item()} and
117
	 *                    {@link remove_menu_item}. Also used as a CSS-class for icon customization.
118
	 * @param string $menuTitle Localized title showing in the menu bar
119
	 * @param string $url A relative URL that will be linked in the menu bar.
120
	 * @param string $controllerClass The controller class for this menu, used to check permisssions.
121
	 *                    If blank, it's assumed that this is public, and always shown to users who
122
	 *                    have the rights to access some other part of the admin area.
123
	 * @param int $priority
124
	 * @param array $attributes an array of attributes to include on the link.
125
	 * @return bool Success
126
	 */
127
	public static function add_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1,
128
											$attributes = null) {
129
		// If a class is defined, then force the use of that as a code.  This helps prevent menu item duplication
130
		if($controllerClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $controllerClass of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
131
			$code = self::get_menu_code($controllerClass);
132
		}
133
134
		return self::replace_menu_item($code, $menuTitle, $url, $controllerClass, $priority, $attributes);
0 ignored issues
show
Bug introduced by
It seems like $code defined by self::get_menu_code($controllerClass) on line 131 can also be of type array; however, CMSMenu::replace_menu_item() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
135
	}
136
137
	/**
138
	 * Get a single menu item by its code value.
139
	 *
140
	 * @param string $code
141
	 * @return array
142
	 */
143
	public static function get_menu_item($code) {
144
		$menuItems = self::get_menu_items();
145
		return (isset($menuItems[$code])) ? $menuItems[$code] : false;
146
	}
147
148
	/**
149
	 * Get menu code for class
150
	 *
151
	 * @param string $cmsClass Controller class name
152
	 * @return string
153
	 */
154
	public static function get_menu_code($cmsClass) {
155
		return Convert::raw2htmlname(str_replace('\\', '-', $cmsClass));
156
	}
157
158
	/**
159
	 * Get all menu entries.
160
	 *
161
	 * @return array
162
	 */
163
	public static function get_menu_items() {
164
		$menuItems = array();
165
166
		// Set up default menu items
167
		if(!self::$menu_is_cleared) {
168
			$cmsClasses = self::get_cms_classes();
169
			foreach($cmsClasses as $cmsClass) {
170
				$menuItem = self::menuitem_for_controller($cmsClass);
171
				$menuCode = self::get_menu_code($cmsClass);
172
				if($menuItem) {
173
					$menuItems[$menuCode] = $menuItem;
174
				}
175
			}
176
		}
177
178
		// Apply changes
179
		foreach(self::$menu_item_changes as $change) {
180
			switch($change['type']) {
181
				case 'add':
182
					$menuItems[$change['code']] = $change['item'];
183
					break;
184
185
				case 'remove':
186
					unset($menuItems[$change['code']]);
187
					break;
188
189
				default:
190
					user_error("Bad menu item change type {$change[type]}", E_USER_WARNING);
191
			}
192
		}
193
194
		// Sort menu items according to priority, then title asc
195
		$menuPriority = array();
196
		$menuTitle    = array();
197
		foreach($menuItems as $key => $menuItem) {
198
			$menuPriority[$key] = is_numeric($menuItem->priority) ? $menuItem->priority : 0;
199
			$menuTitle[$key]    = $menuItem->title;
200
		}
201
		array_multisort($menuPriority, SORT_DESC, $menuTitle, SORT_ASC, $menuItems);
202
203
		return $menuItems;
204
	}
205
206
	/**
207
	 * Get all menu items that the passed member can view.
208
	 * Defaults to {@link Member::currentUser()}.
209
	 *
210
	 * @param Member $member
211
	 * @return array
212
	 */
213
	public static function get_viewable_menu_items($member = null) {
214
		if(!$member && $member !== FALSE) {
215
			$member = Member::currentUser();
216
		}
217
218
		$viewableMenuItems = array();
219
		$allMenuItems = self::get_menu_items();
220
		if($allMenuItems) foreach($allMenuItems as $code => $menuItem) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allMenuItems of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
221
			// exclude all items which have a controller to perform permission
222
			// checks on
223
			if($menuItem->controller) {
224
				$controllerObj = singleton($menuItem->controller);
225
				if(Controller::has_curr()) {
226
					// Necessary for canView() to have request data available,
227
					// e.g. to check permissions against LeftAndMain->currentPage()
228
					$controllerObj->setRequest(Controller::curr()->getRequest());
229
					if(!$controllerObj->canView($member)) continue;
230
				}
231
			}
232
233
			$viewableMenuItems[$code] = $menuItem;
234
		}
235
236
		return $viewableMenuItems;
237
	}
238
239
	/**
240
	 * Removes an existing item from the menu.
241
	 *
242
	 * @param string $code Unique identifier for this menu item
243
	 */
244
	public static function remove_menu_item($code) {
245
		self::$menu_item_changes[] = array('type' => 'remove', 'code' => $code);
246
	}
247
248
	/**
249
	 * Remove menu item by class name.
250
	 *
251
	 * @param string $className Name of class
252
	 */
253
	public static function remove_menu_class($className) {
254
		$code = self::get_menu_code($className);
255
		self::remove_menu_item($code);
0 ignored issues
show
Bug introduced by
It seems like $code defined by self::get_menu_code($className) on line 254 can also be of type array; however, CMSMenu::remove_menu_item() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
256
	}
257
258
	/**
259
	 * Clears the entire menu
260
	 */
261
	public static function clear_menu() {
262
		self::$menu_item_changes = array();
263
		self::$menu_is_cleared = true;
264
	}
265
266
	/**
267
	 * Replace a navigation item to the main administration menu showing in the top bar.
268
	 *
269
	 * @param string $code Unique identifier for this menu item (e.g. used by {@link replace_menu_item()} and
270
	 *                    {@link remove_menu_item}. Also used as a CSS-class for icon customization.
271
	 * @param string $menuTitle Localized title showing in the menu bar
272
	 * @param string $url A relative URL that will be linked in the menu bar.
273
	 *                    Make sure to add a matching route via {@link Director::$rules} to this url.
274
	 * @param string $controllerClass The controller class for this menu, used to check permisssions.
275
	 *                    If blank, it's assumed that this is public, and always shown to users who
276
	 *                    have the rights to access some other part of the admin area.
277
	 * @param int $priority
278
	 * @param array $attributes an array of attributes to include on the link.
279
	 * @return bool Success
280
	 */
281
	public static function replace_menu_item($code, $menuTitle, $url, $controllerClass = null, $priority = -1,
282
												$attributes = null) {
283
		$item = new CMSMenuItem($menuTitle, $url, $controllerClass, $priority);
284
285
		if($attributes) {
286
			$item->setAttributes($attributes);
287
		}
288
289
		self::$menu_item_changes[] = array(
290
			'type' => 'add',
291
			'code' => $code,
292
			'item' => $item,
293
		);
294
	}
295
296
	/**
297
	 * Add a previously built menu item object to the menu
298
	 *
299
	 * @param string $code
300
	 * @param CMSMenuItem $cmsMenuItem
301
	 */
302
	protected static function add_menu_item_obj($code, $cmsMenuItem) {
303
		self::$menu_item_changes[] = array(
304
			'type' => 'add',
305
			'code' => $code,
306
			'item' => $cmsMenuItem,
307
		);
308
	}
309
310
	/**
311
	 * A utility funciton to retrieve subclasses of a given class that
312
	 * are instantiable (ie, not abstract) and have a valid menu title.
313
	 *
314
	 * Sorted by url_priority config.
315
	 *
316
	 * @todo A variation of this function could probably be moved to {@link ClassInfo}
317
	 * @param string $root The root class to begin finding subclasses
318
	 * @param boolean $recursive Look for subclasses recursively?
319
	 * @param string $sort Name of config on which to sort. Can be 'menu_priority' or 'url_priority'
320
	 * @return array Valid, unique subclasses
321
	 */
322
	public static function get_cms_classes($root = null, $recursive = true, $sort = self::MENU_PRIORITY) {
323
		if(!$root) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $root of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
324
			$root = 'LeftAndMain';
325
		}
326
		$abstractClasses = ['LeftAndMain', 'CMSMain'];
327
		$subClasses = array_values(ClassInfo::subclassesFor($root));
328
		foreach($subClasses as $className) {
329
			if($recursive && $className != $root) {
330
				$subClasses = array_merge($subClasses, array_values(ClassInfo::subclassesFor($className)));
331
			}
332
		}
333
		$subClasses = array_unique($subClasses);
334
		foreach($subClasses as $key => $className) {
335
			// Remove abstract classes and LeftAndMain
336
			if(in_array($className, $abstractClasses) || ClassInfo::classImplements($className, 'TestOnly')) {
337
				unset($subClasses[$key]);
338
			} else {
339
				// Separate conditional to avoid autoloading the class
340
				$classReflection = new ReflectionClass($className);
341
				if(!$classReflection->isInstantiable()) {
342
					unset($subClasses[$key]);
343
				}
344
			}
345
		}
346
347
		// Sort by specified sorting config
348
		usort($subClasses, function ($a, $b) use ($sort) {
349
			$priorityA = Config::inst()->get($a, $sort);
350
			$priorityB = Config::inst()->get($b, $sort);
351
			return $priorityB - $priorityA;
352
		});
353
354
		return $subClasses;
355
	}
356
357
	/**
358
	 * IteratorAggregate Interface Method.  Iterates over the menu items.
359
	 */
360
	public function getIterator() {
361
		return new ArrayIterator(self::get_menu_items());
362
	}
363
364
	/**
365
	 * Provide menu titles to the i18n entity provider
366
	 */
367
	public function provideI18nEntities() {
368
		$cmsClasses = self::get_cms_classes();
369
		$entities = array();
370
		foreach($cmsClasses as $cmsClass) {
371
			$defaultTitle = LeftAndMain::menu_title($cmsClass, false);
372
			$ownerModule = i18n::get_owner_module($cmsClass);
373
			$entities["{$cmsClass}.MENUTITLE"] = array($defaultTitle, 'Menu title', $ownerModule);
374
		}
375
		return $entities;
376
	}
377
}
378