Admin Menu Editor - Version 1.8.3

Version Description

  • Added a couple of tutorial links to the settings page.
  • Fixed a potential crash that was caused by a bug in the "WP Editor" plugin version 1.2.6.3.
  • Fixed some obsolete callback syntax that was still using "&$this".
  • Changed the order of some menu settings and added separators between groups of settings.
  • Removed the "Screen Options" panel from AME tabs that didn't need it like "Plugins".
  • Tested with WP 4.9.5.
Download this release

Release Info

Developer whiteshadow
Plugin Icon 128x128 Admin Menu Editor
Version 1.8.3
Comparing to
See all releases

Code changes from version 1.8.2 to 1.8.3

css/_test-access-screen.scss ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*********************************************
2
+ "Test access" dialog
3
+ **********************************************/
4
+
5
+ #ws_ame_test_access_screen {
6
+ display: none;
7
+ background: #fcfcfc;
8
+ }
9
+
10
+ #ws_ame_test_inputs {
11
+ //border-bottom: 1px solid #ddd;
12
+ padding-bottom: 16px;
13
+ }
14
+
15
+ .ws_ame_test_input {
16
+ display: block;
17
+ float: left;
18
+
19
+ width: 100%;
20
+ margin: 2px 0;
21
+ box-sizing: content-box;
22
+ }
23
+
24
+ .ws_ame_test_input_name {
25
+ display: block;
26
+ float: left;
27
+ width: 35%;
28
+ margin-right: 4%;
29
+
30
+ text-align: right;
31
+ padding-top: 6px;
32
+ line-height: 16px;
33
+ }
34
+
35
+ .ws_ame_test_input_value {
36
+ display: block;
37
+ float: right;
38
+ width: 60%;
39
+
40
+ -webkit-box-sizing: border-box;
41
+ -moz-box-sizing: border-box;
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ #ws_ame_test_actions {
46
+ float: left;
47
+ width: 100%;
48
+ margin-top: 1em;
49
+ }
50
+
51
+ #ws_ame_test_button_container {
52
+ width: 35%;
53
+ margin-right: 4%;
54
+ float: left;
55
+ text-align: right;
56
+ }
57
+
58
+ #ws_ame_test_progress {
59
+ display: none;
60
+ width: 60%;
61
+ float: right;
62
+
63
+ .spinner {
64
+ float: none;
65
+ vertical-align: bottom;
66
+ margin-left: 0;
67
+ margin-right: 4px;
68
+ }
69
+ }
70
+
71
+ #ws_ame_test_access_body {
72
+ width: 100%;
73
+ position: relative;
74
+
75
+ border: 1px solid #ddd;
76
+ -webkit-border-radius: 3px;
77
+ -moz-border-radius: 3px;
78
+ border-radius: 3px;
79
+ }
80
+
81
+ #ws_ame_test_frame_container {
82
+ margin-right: 250px;
83
+ background: white;
84
+
85
+ min-height: 500px;
86
+ position: relative;
87
+ }
88
+
89
+ #ws_ame_test_access_frame {
90
+ -webkit-box-sizing: border-box;
91
+ -moz-box-sizing: border-box;
92
+ box-sizing: border-box;
93
+
94
+ width: 100%;
95
+ height: 100%;
96
+ min-height: 500px;
97
+
98
+ border: none;
99
+ margin: 0;
100
+ padding: 0;
101
+ }
102
+
103
+ #ws_ame_test_access_sidebar {
104
+ -webkit-box-sizing: border-box;
105
+ -moz-box-sizing: border-box;
106
+ box-sizing: border-box;
107
+
108
+ position: absolute;
109
+ top: 0;
110
+ right: 0;
111
+ bottom: 0;
112
+
113
+ width: 250px;
114
+ padding: 16px 24px;
115
+
116
+ background-color: #f3f3f3;
117
+ border-left: 1px solid #ddd;
118
+
119
+ h4:first-of-type {
120
+ margin-top: 0;
121
+ }
122
+ }
123
+
124
+ #ws_ame_test_frame_placeholder {
125
+ display: block;
126
+ padding: 16px 24px;
127
+ }
128
+
129
+ #ws_ame_test_output {
130
+ display: none;
131
+ }
css/menu-editor.css CHANGED
@@ -272,6 +272,19 @@
272
  height: 25px;
273
  min-height: 25px; }
274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  /* The reset-to-default button */
276
  .ws_reset_button {
277
  display: block;
@@ -805,16 +818,20 @@ a#ws-ame-delete-color-preset:hover {
805
  /* Color scheme display in the editor widget. */
806
  .ws_color_scheme_display {
807
  display: inline-block;
808
- height: 20px;
809
- width: 186px;
 
810
  margin-right: 5px;
811
- padding: 2px 3px;
812
  font-size: 12px;
813
  border: 1px solid #ddd;
814
  background: white;
815
  cursor: pointer;
816
  line-height: 20px; }
817
 
 
 
 
818
  .ws_color_display_item {
819
  display: inline-block;
820
  width: 18px;
@@ -956,7 +973,8 @@ a#ws-ame-delete-color-preset:hover {
956
  margin-right: 5px; }
957
 
958
  .ws_launch_access_editor {
959
- min-width: 40px; }
 
960
 
961
  #ws_menu_access_editor {
962
  width: 400px;
@@ -1408,6 +1426,22 @@ a#ws-ame-delete-color-preset:hover {
1408
  margin-top: 0;
1409
  margin-bottom: 1em; }
1410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1411
  /*********************************************
1412
  Miscellaneous
1413
  **********************************************/
@@ -1441,4 +1475,106 @@ a#ws-ame-delete-color-preset:hover {
1441
  .test-content {
1442
  padding: 8px; }
1443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1444
  /*# sourceMappingURL=menu-editor.css.map */
272
  height: 25px;
273
  min-height: 25px; }
274
 
275
+ /*
276
+ * Group headings
277
+ */
278
+ .ws_edit_field.ws_field_group_heading {
279
+ height: 1px;
280
+ min-height: 0;
281
+ padding-top: 0;
282
+ background: #ccc;
283
+ margin: 8px -4px 5px; }
284
+ .ws_edit_field.ws_field_group_heading span {
285
+ display: none;
286
+ font-weight: bold; }
287
+
288
  /* The reset-to-default button */
289
  .ws_reset_button {
290
  display: block;
818
  /* Color scheme display in the editor widget. */
819
  .ws_color_scheme_display {
820
  display: inline-block;
821
+ box-sizing: border-box;
822
+ height: 26px;
823
+ width: 190px;
824
  margin-right: 5px;
825
+ padding: 2px 4px;
826
  font-size: 12px;
827
  border: 1px solid #ddd;
828
  background: white;
829
  cursor: pointer;
830
  line-height: 20px; }
831
 
832
+ .ws_open_color_editor {
833
+ width: 58px; }
834
+
835
  .ws_color_display_item {
836
  display: inline-block;
837
  width: 18px;
973
  margin-right: 5px; }
974
 
975
  .ws_launch_access_editor {
976
+ min-width: 40px;
977
+ width: 58px; }
978
 
979
  #ws_menu_access_editor {
980
  width: 400px;
1426
  margin-top: 0;
1427
  margin-bottom: 1em; }
1428
 
1429
+ .ame-available-add-ons tr:first-of-type td {
1430
+ margin-top: 0;
1431
+ padding-top: 0; }
1432
+ .ame-available-add-ons td {
1433
+ padding-top: 10px;
1434
+ padding-bottom: 10px; }
1435
+ .ame-available-add-ons .ame-add-on-heading {
1436
+ padding-left: 0; }
1437
+
1438
+ .ame-add-on-name {
1439
+ font-weight: 600; }
1440
+
1441
+ .ame-add-on-details-link::after {
1442
+ /*content: " \f504";
1443
+ font-family: dashicons, sans-serif;*/ }
1444
+
1445
  /*********************************************
1446
  Miscellaneous
1447
  **********************************************/
1475
  .test-content {
1476
  padding: 8px; }
1477
 
1478
+ /*********************************************
1479
+ "Test access" dialog
1480
+ **********************************************/
1481
+ #ws_ame_test_access_screen {
1482
+ display: none;
1483
+ background: #fcfcfc; }
1484
+
1485
+ #ws_ame_test_inputs {
1486
+ padding-bottom: 16px; }
1487
+
1488
+ .ws_ame_test_input {
1489
+ display: block;
1490
+ float: left;
1491
+ width: 100%;
1492
+ margin: 2px 0;
1493
+ box-sizing: content-box; }
1494
+
1495
+ .ws_ame_test_input_name {
1496
+ display: block;
1497
+ float: left;
1498
+ width: 35%;
1499
+ margin-right: 4%;
1500
+ text-align: right;
1501
+ padding-top: 6px;
1502
+ line-height: 16px; }
1503
+
1504
+ .ws_ame_test_input_value {
1505
+ display: block;
1506
+ float: right;
1507
+ width: 60%;
1508
+ -webkit-box-sizing: border-box;
1509
+ -moz-box-sizing: border-box;
1510
+ box-sizing: border-box; }
1511
+
1512
+ #ws_ame_test_actions {
1513
+ float: left;
1514
+ width: 100%;
1515
+ margin-top: 1em; }
1516
+
1517
+ #ws_ame_test_button_container {
1518
+ width: 35%;
1519
+ margin-right: 4%;
1520
+ float: left;
1521
+ text-align: right; }
1522
+
1523
+ #ws_ame_test_progress {
1524
+ display: none;
1525
+ width: 60%;
1526
+ float: right; }
1527
+ #ws_ame_test_progress .spinner {
1528
+ float: none;
1529
+ vertical-align: bottom;
1530
+ margin-left: 0;
1531
+ margin-right: 4px; }
1532
+
1533
+ #ws_ame_test_access_body {
1534
+ width: 100%;
1535
+ position: relative;
1536
+ border: 1px solid #ddd;
1537
+ -webkit-border-radius: 3px;
1538
+ -moz-border-radius: 3px;
1539
+ border-radius: 3px; }
1540
+
1541
+ #ws_ame_test_frame_container {
1542
+ margin-right: 250px;
1543
+ background: white;
1544
+ min-height: 500px;
1545
+ position: relative; }
1546
+
1547
+ #ws_ame_test_access_frame {
1548
+ -webkit-box-sizing: border-box;
1549
+ -moz-box-sizing: border-box;
1550
+ box-sizing: border-box;
1551
+ width: 100%;
1552
+ height: 100%;
1553
+ min-height: 500px;
1554
+ border: none;
1555
+ margin: 0;
1556
+ padding: 0; }
1557
+
1558
+ #ws_ame_test_access_sidebar {
1559
+ -webkit-box-sizing: border-box;
1560
+ -moz-box-sizing: border-box;
1561
+ box-sizing: border-box;
1562
+ position: absolute;
1563
+ top: 0;
1564
+ right: 0;
1565
+ bottom: 0;
1566
+ width: 250px;
1567
+ padding: 16px 24px;
1568
+ background-color: #f3f3f3;
1569
+ border-left: 1px solid #ddd; }
1570
+ #ws_ame_test_access_sidebar h4:first-of-type {
1571
+ margin-top: 0; }
1572
+
1573
+ #ws_ame_test_frame_placeholder {
1574
+ display: block;
1575
+ padding: 16px 24px; }
1576
+
1577
+ #ws_ame_test_output {
1578
+ display: none; }
1579
+
1580
  /*# sourceMappingURL=menu-editor.css.map */
css/menu-editor.scss CHANGED
@@ -393,6 +393,24 @@ $mainContainerBorderRadius: 0px;
393
  min-height: 25px;
394
  }
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  /* The reset-to-default button */
397
  .ws_reset_button {
398
  display: block;
@@ -427,11 +445,12 @@ $mainContainerBorderRadius: 0px;
427
  }
428
 
429
  /* The input box in each field editor */
 
430
  #ws_menu_editor .ws_editbox input[type="text"],
431
  #ws_menu_editor .ws_editbox select {
432
  display: block;
433
  float: left;
434
- width: 254px;
435
  height: 25px;
436
 
437
  font-size: 12px;
@@ -1088,22 +1107,30 @@ a#ws-ame-delete-color-preset:hover {
1088
 
1089
  /* Color scheme display in the editor widget. */
1090
 
 
 
 
1091
  .ws_color_scheme_display {
1092
- $colorFieldHeight: 20px;
1093
 
1094
  display: inline-block;
 
1095
  height: $colorFieldHeight;
1096
- width: 186px;
1097
 
1098
- margin-right: 5px;
1099
- padding: 2px 3px;
1100
 
1101
  font-size: 12px;
1102
  border: 1px solid #ddd;
1103
  background: white;
1104
  cursor: pointer;
1105
 
1106
- line-height: $colorFieldHeight;
 
 
 
 
1107
  }
1108
 
1109
  .ws_color_display_item {
@@ -1285,14 +1312,17 @@ a#ws-ame-delete-color-preset:hover {
1285
  *************************************/
1286
 
1287
  /* The launch button */
 
 
1288
  #ws_menu_editor .ws_edit_field-access_level input.ws_field_value
1289
  {
1290
- width: 190px;
1291
- margin-right: 5px;
1292
  }
1293
 
1294
  .ws_launch_access_editor {
1295
  min-width: 40px;
 
1296
  }
1297
 
1298
  #ws_menu_access_editor {
@@ -1991,6 +2021,31 @@ $userSelectionPanelPadding: 10px;
1991
  margin-bottom: 1em;
1992
  }
1993
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1994
  /*********************************************
1995
  Miscellaneous
1996
  **********************************************/
@@ -2034,4 +2089,6 @@ $userSelectionPanelPadding: 10px;
2034
 
2035
  .test-content {
2036
  padding: 8px;
2037
- }
 
 
393
  min-height: 25px;
394
  }
395
 
396
+ /*
397
+ * Group headings
398
+ */
399
+ .ws_edit_field.ws_field_group_heading {
400
+ //display: none;
401
+ height: 1px;
402
+ min-height: 0;
403
+ padding-top: 0;
404
+
405
+ background: #ccc;
406
+ margin: 8px -4px 5px;
407
+
408
+ & span {
409
+ display: none;
410
+ font-weight: bold;
411
+ }
412
+ }
413
+
414
  /* The reset-to-default button */
415
  .ws_reset_button {
416
  display: block;
445
  }
446
 
447
  /* The input box in each field editor */
448
+ $basicInputWidth: 254px;
449
  #ws_menu_editor .ws_editbox input[type="text"],
450
  #ws_menu_editor .ws_editbox select {
451
  display: block;
452
  float: left;
453
+ width: $basicInputWidth;
454
  height: 25px;
455
 
456
  font-size: 12px;
1107
 
1108
  /* Color scheme display in the editor widget. */
1109
 
1110
+ $colorFieldWidth: 190px;
1111
+ $colorFieldRightMargin: 5px;
1112
+
1113
  .ws_color_scheme_display {
1114
+ $colorFieldHeight: 26px;
1115
 
1116
  display: inline-block;
1117
+ box-sizing: border-box;
1118
  height: $colorFieldHeight;
1119
+ width: $colorFieldWidth;
1120
 
1121
+ margin-right: $colorFieldRightMargin;
1122
+ padding: 2px 4px;
1123
 
1124
  font-size: 12px;
1125
  border: 1px solid #ddd;
1126
  background: white;
1127
  cursor: pointer;
1128
 
1129
+ line-height: $colorFieldHeight - 6px;
1130
+ }
1131
+
1132
+ .ws_open_color_editor {
1133
+ width: $basicInputWidth - $colorFieldWidth - $colorFieldRightMargin - 1px;
1134
  }
1135
 
1136
  .ws_color_display_item {
1312
  *************************************/
1313
 
1314
  /* The launch button */
1315
+ $accessLevelInputWidth: 190px;
1316
+ $accessLevelRightMargin: 5px;
1317
  #ws_menu_editor .ws_edit_field-access_level input.ws_field_value
1318
  {
1319
+ width: $accessLevelInputWidth;
1320
+ margin-right: $accessLevelRightMargin;
1321
  }
1322
 
1323
  .ws_launch_access_editor {
1324
  min-width: 40px;
1325
+ width: $basicInputWidth - $accessLevelRightMargin - $accessLevelInputWidth - 1px;
1326
  }
1327
 
1328
  #ws_menu_access_editor {
2021
  margin-bottom: 1em;
2022
  }
2023
 
2024
+ .ame-available-add-ons {
2025
+ tr:first-of-type td {
2026
+ margin-top: 0;
2027
+ padding-top: 0;
2028
+ }
2029
+
2030
+ td {
2031
+ padding-top: 10px;
2032
+ padding-bottom: 10px;
2033
+ }
2034
+
2035
+ .ame-add-on-heading {
2036
+ padding-left: 0;
2037
+ }
2038
+ }
2039
+
2040
+ .ame-add-on-name {
2041
+ font-weight: 600;
2042
+ }
2043
+
2044
+ .ame-add-on-details-link::after {
2045
+ /*content: " \f504";
2046
+ font-family: dashicons, sans-serif;*/
2047
+ }
2048
+
2049
  /*********************************************
2050
  Miscellaneous
2051
  **********************************************/
2089
 
2090
  .test-content {
2091
  padding: 8px;
2092
+ }
2093
+
2094
+ @import "test-access-screen";
includes/access-test-runner.php ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ameAccessTestRunner implements ArrayAccess {
4
+ const TEST_DATA_META_KEY = 'ws_ame_access_test_data';
5
+
6
+ /**
7
+ * @var WPMenuEditor
8
+ */
9
+ private $menuEditor;
10
+
11
+ private $get = array();
12
+
13
+ private $test_menu = null;
14
+ private $test_target_item = null;
15
+ private $test_target_parent = null;
16
+ private $test_relevant_role = null;
17
+
18
+ private $original_wp_die_handler = null;
19
+ private $access_test_results = array();
20
+
21
+ public function __construct($menuEditor, $queryParameters) {
22
+ $this->menuEditor = $menuEditor;
23
+ $this->get = $queryParameters;
24
+
25
+ add_filter('admin_menu_editor-script_data', array($this, 'addEditorScriptData'));
26
+
27
+ add_action('wp_ajax_ws_ame_set_test_configuration', array($this, 'ajax_set_test_configuration'));
28
+ add_action('set_current_user', array($this, 'init_access_test'));
29
+ }
30
+
31
+ public function addEditorScriptData($scriptData) {
32
+ $scriptData = array_merge(
33
+ $scriptData,
34
+ array(
35
+ 'setTestConfigurationNonce' => wp_create_nonce('ws_ame_set_test_configuration'),
36
+ 'testAccessNonce' => wp_create_nonce('ws_ame_test_access'),
37
+ )
38
+ );
39
+ return $scriptData;
40
+ }
41
+
42
+ public function ajax_set_test_configuration() {
43
+ check_ajax_referer('ws_ame_set_test_configuration');
44
+ if ( !$this->menuEditor->current_user_can_edit_menu() ) {
45
+ exit($this->menuEditor->json_encode(array(
46
+ 'error' => 'You don\'t have permission to test menu settings.',
47
+ )));
48
+ }
49
+
50
+ $post = $this->menuEditor->get_post_params();
51
+ $menuData = strval($post['data']);
52
+
53
+ $metaId = add_user_meta(get_current_user_id(), self::TEST_DATA_META_KEY, wp_slash($menuData), false);
54
+ if ( $metaId === false ) {
55
+ exit($this->menuEditor->json_encode(array(
56
+ 'error' => 'Failed to store test data. add_user_meta() returned FALSE.',
57
+ )));
58
+ }
59
+
60
+ exit($this->menuEditor->json_encode(array(
61
+ 'success' => true,
62
+ 'meta_id' => $metaId,
63
+ )));
64
+ }
65
+
66
+ public function init_access_test() {
67
+ //We want to do this only once per page load: specifically, when WP authenticates
68
+ //the user at the start of the request.
69
+ static $is_user_already_set = false;
70
+ if ( $is_user_already_set || $this->menuEditor->is_access_test || did_action('init') ) {
71
+ return;
72
+ }
73
+ $is_user_already_set = true;
74
+
75
+ if (
76
+ !isset(
77
+ $this->get['ame-test-menu-access-as'],
78
+ $this->get['ame-test-target-item']
79
+ )
80
+ || !check_admin_referer('ws_ame_test_access')
81
+ ) {
82
+ return;
83
+ }
84
+
85
+
86
+ $configurations = get_user_meta(get_current_user_id(), self::TEST_DATA_META_KEY, false);
87
+ if ( empty($configurations) ) {
88
+ exit('Error: Test data not found.');
89
+ }
90
+
91
+ //Use the most recent config. It's usually the last one.
92
+ $json = array_pop($configurations);
93
+ //Clean up the database.
94
+ delete_user_meta(get_current_user_id(), self::TEST_DATA_META_KEY, wp_slash($json));
95
+
96
+ try {
97
+ $test_menu = ameMenu::load_json($json);
98
+ } catch (InvalidMenuException $e) {
99
+ exit($e->getMessage());
100
+ }
101
+ $this->test_menu = $test_menu;
102
+
103
+ $user = get_user_by('login', strval($this->get['ame-test-menu-access-as']));
104
+ if ( !$user ) {
105
+ exit('Error: User not found.');
106
+ }
107
+
108
+ //Everything looks good, proceed with the test.
109
+ $this->menuEditor->is_access_test = true;
110
+
111
+ $this->access_test_results = array();
112
+ $this->test_target_item = strval($this->get['ame-test-target-item']);
113
+ $this->test_target_parent = ameUtils::get($this->get, 'ame-test-target-parent', null);
114
+ $this->test_relevant_role = ameUtils::get($this->get, 'ame-test-relevant-role', null);
115
+
116
+ if ( $this->test_target_parent === '' ) {
117
+ $this->test_target_parent = null;
118
+ }
119
+ if ( $this->test_relevant_role === null ) {
120
+ $this->test_relevant_role = null;
121
+ }
122
+
123
+ wp_set_current_user($user->ID, $user->user_login);
124
+
125
+ $this->menuEditor->set_plugin_option('security_logging_enabled', true);
126
+ add_action('admin_print_scripts', array($this, 'output_access_test_results'));
127
+ add_filter('wp_die_handler', array($this, 'replace_die_handler_for_access_test'), 25, 1);
128
+ }
129
+
130
+ public function output_access_test_results() {
131
+ echo $this->get_access_test_result_script();
132
+ }
133
+
134
+ private function get_access_test_result_script() {
135
+ $response = array_merge(
136
+ $this->access_test_results,
137
+ array(
138
+ 'securityLog' => $this->menuEditor->get_security_log(),
139
+ )
140
+ );
141
+
142
+ $script = '<script type="text/javascript">
143
+ window.parent.postMessage((' . $this->menuEditor->json_encode($response) . '), "*");
144
+ </script>';
145
+ return $script;
146
+ }
147
+
148
+ public function replace_die_handler_for_access_test($callback = null) {
149
+ $this->original_wp_die_handler = $callback;
150
+ return array($this, 'die_during_an_access_test');
151
+ }
152
+
153
+ public function die_during_an_access_test($message, $title = '', $args = array()) {
154
+ if ( $this->original_wp_die_handler ) {
155
+ $script = $this->get_access_test_result_script();
156
+ if ( $message instanceof WP_Error ) {
157
+ $message->add('ame-access-test-response', '[Access test]' . $script);
158
+ } else if ( is_string($message) ) {
159
+ $message .= $script;
160
+ }
161
+
162
+ call_user_func($this->original_wp_die_handler, $message, $title, $args);
163
+ } else {
164
+ exit('Unexpected error: wp_die() was called but there is no default handler.');
165
+ }
166
+ }
167
+
168
+ private function find_target_menu_item($items, $item_file, $parent_file = null, $current_parent = null) {
169
+ foreach ($items as $item) {
170
+ $this_file = ameMenuItem::get($item, 'file', null);
171
+ if ( ($this_file === $item_file) && ($parent_file === $current_parent) ) {
172
+ return $item;
173
+ }
174
+
175
+ if ( !empty($item['items']) ) {
176
+ $result = $this->find_target_menu_item($item['items'], $item_file, $parent_file, $this_file);
177
+ if ( $result !== null ) {
178
+ return $result;
179
+ }
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+
185
+ public function setCurrentMenuItem($menuItem) {
186
+ $this->access_test_results['currentMenuItem'] = $menuItem;
187
+
188
+ $this->access_test_results['currentMenuItemIsTarget'] =
189
+ isset($this->access_test_results['currentMenuItem'])
190
+ && (ameMenuItem::get($this->access_test_results['currentMenuItem'], 'file', null) === $this->test_target_item)
191
+ && (ameMenuItem::get($this->access_test_results['currentMenuItem'], 'parent', null) === $this->test_target_parent);
192
+
193
+ $this->access_test_results['isIdentity'] =
194
+ ($this->access_test_results['currentMenuItem'] === $this->access_test_results['targetMenuItem']);
195
+ }
196
+
197
+ public function onFinalTreeReady($tree) {
198
+ //Find the target item. It might not be the same as the current item.
199
+ $this->access_test_results['targetMenuItem'] = $this->find_target_menu_item(
200
+ $tree,
201
+ $this->test_target_item,
202
+ $this->test_target_parent
203
+ );
204
+ }
205
+
206
+
207
+ /**
208
+ * Whether a offset exists
209
+ *
210
+ * @link http://php.net/manual/en/arrayaccess.offsetexists.php
211
+ * @param mixed $offset <p>
212
+ * An offset to check for.
213
+ * </p>
214
+ * @return boolean true on success or false on failure.
215
+ * </p>
216
+ * <p>
217
+ * The return value will be casted to boolean if non-boolean was returned.
218
+ * @since 5.0.0
219
+ */
220
+ public function offsetExists($offset) {
221
+ return array_key_exists($offset, $this->access_test_results);
222
+ }
223
+
224
+ /**
225
+ * Offset to retrieve
226
+ *
227
+ * @link http://php.net/manual/en/arrayaccess.offsetget.php
228
+ * @param mixed $offset <p>
229
+ * The offset to retrieve.
230
+ * </p>
231
+ * @return mixed Can return all value types.
232
+ * @since 5.0.0
233
+ */
234
+ public function offsetGet($offset) {
235
+ return $this->access_test_results[$offset];
236
+ }
237
+
238
+ /**
239
+ * Offset to set
240
+ *
241
+ * @link http://php.net/manual/en/arrayaccess.offsetset.php
242
+ * @param mixed $offset <p>
243
+ * The offset to assign the value to.
244
+ * </p>
245
+ * @param mixed $value <p>
246
+ * The value to set.
247
+ * </p>
248
+ * @return void
249
+ * @since 5.0.0
250
+ */
251
+ public function offsetSet($offset, $value) {
252
+ $this->access_test_results[$offset] = $value;
253
+ }
254
+
255
+ /**
256
+ * Offset to unset
257
+ *
258
+ * @link http://php.net/manual/en/arrayaccess.offsetunset.php
259
+ * @param mixed $offset <p>
260
+ * The offset to unset.
261
+ * </p>
262
+ * @return void
263
+ * @since 5.0.0
264
+ */
265
+ public function offsetUnset($offset) {
266
+ unset($this->access_test_results[$offset]);
267
+ }
268
+ }
includes/editor-page.php CHANGED
@@ -270,6 +270,10 @@ function ame_output_sort_buttons($icons) {
270
  <input type="button" id='ws_reset_menu' value="Undo changes" class="button ws_main_button" />
271
  <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
272
 
 
 
 
 
273
  <?php
274
  $compact_layout_title = 'Compact layout';
275
  if ( $is_compact_layout_enabled ) {
@@ -309,17 +313,23 @@ function ame_output_sort_buttons($icons) {
309
  <?php
310
  endif;
311
 
312
- if ( $is_pro_version ) :
313
- $is_how_to_box_open = true;
314
- if ( isset($_COOKIE['ame_how_to_box_open']) ) {
315
- $is_how_to_box_open = ($_COOKIE['ame_how_to_box_open'] === '1');
316
- }
317
- $box_class = $is_how_to_box_open ? '' : 'closed';
318
 
319
- $how_to_link_template = '<a href="https://adminmenueditor.com/documentation/%1$s" target="_blank" title="Opens in a new tab">%2$s</a>';
320
- $how_to_item_template = '<li>' . $how_to_link_template . '</li>';
 
 
 
321
 
322
- ?>
 
 
 
 
323
  <div class="postbox ws_ame_custom_postbox <?php echo $box_class; ?>" id="ws_ame_how_to_box">
324
  <button type="button" class="handlediv button-link">
325
  <span class="toggle-indicator"></span>
@@ -327,43 +337,58 @@ function ame_output_sort_buttons($icons) {
327
  <h2 class="hndle">How To</h2>
328
  <div class="inside">
329
  <ul class="ame-tutorial-list">
330
- <li><?php
331
- printf($how_to_link_template, 'how-to-hide-a-menu-item/', 'Hide a Menu...');
 
332
  ?>
333
- <ul class="ame-tutorial-list">
334
- <?php
335
- foreach (
336
- array(
337
- 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-a-role' => 'From a Role',
338
- 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-a-user' => 'From a User',
339
- 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-everyone-except-yourself' => 'From Everyone Except You',
340
- 'how-to-hide-menu-without-preventing-access/' => 'Without Preventing Access',
341
- )
342
- as $how_to_url => $how_to_title
343
- ) {
344
- printf($how_to_item_template, esc_attr($how_to_url), $how_to_title);
345
- }
346
  ?>
347
- </ul>
348
- </li>
349
- <?php
350
- foreach (
351
- array(
352
- 'how-to-give-access-to-menu/' => 'Show a Menu',
353
- 'how-to-move-and-sort-menus/' => 'Move and Sort Menus',
354
- 'how-to-add-a-new-menu-item/' => 'Add a New Menu',
355
- )
356
- as $how_to_url => $how_to_title
357
- ) {
358
- printf($how_to_item_template, esc_attr($how_to_url), $how_to_title);
359
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  ?>
361
  </ul>
362
  </div>
363
  </div>
364
- <?php
365
- endif;
366
- ?>
367
  </div> <!-- / .metabox-holder -->
368
 
369
  <?php
@@ -664,6 +689,8 @@ function ame_output_sort_buttons($icons) {
664
 
665
  <?php include dirname(__FILE__) . '/cap-suggestion-box.php'; ?>
666
 
 
 
667
  <?php
668
  if ( $is_pro_version ) {
669
  include $extrasDirectory . '/page-dropdown.php';
270
  <input type="button" id='ws_reset_menu' value="Undo changes" class="button ws_main_button" />
271
  <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
272
 
273
+ <!--
274
+ <input type="button" id='ws_test_access' value="Test access..." class="button ws_main_button" />
275
+ -->
276
+
277
  <?php
278
  $compact_layout_title = 'Compact layout';
279
  if ( $is_compact_layout_enabled ) {
313
  <?php
314
  endif;
315
 
316
+ $is_how_to_box_open = true;
317
+ if ( isset($_COOKIE['ame_how_to_box_open']) ) {
318
+ $is_how_to_box_open = ($_COOKIE['ame_how_to_box_open'] === '1');
319
+ }
320
+ $box_class = $is_how_to_box_open ? '' : 'closed';
 
321
 
322
+ if ( $is_pro_version ) {
323
+ $tutorial_base_url = 'https://adminmenueditor.com/documentation/';
324
+ } else {
325
+ $tutorial_base_url = 'https://adminmenueditor.com/free-version-docs/';
326
+ }
327
 
328
+ /** @noinspection HtmlUnknownTarget */
329
+ $how_to_link_template = '<a href="' . htmlspecialchars($tutorial_base_url) . '%1$s" target="_blank" title="Opens in a new tab">%2$s</a>';
330
+ $how_to_item_template = '<li>' . $how_to_link_template . '</li>';
331
+
332
+ ?>
333
  <div class="postbox ws_ame_custom_postbox <?php echo $box_class; ?>" id="ws_ame_how_to_box">
334
  <button type="button" class="handlediv button-link">
335
  <span class="toggle-indicator"></span>
337
  <h2 class="hndle">How To</h2>
338
  <div class="inside">
339
  <ul class="ame-tutorial-list">
340
+ <?php
341
+ if ( $is_pro_version ):
342
+ //Pro version tutorials.
343
  ?>
344
+ <li><?php
345
+ printf($how_to_link_template, 'how-to-hide-a-menu-item/', 'Hide a Menu...');
 
 
 
 
 
 
 
 
 
 
 
346
  ?>
347
+ <ul class="ame-tutorial-list">
348
+ <?php
349
+ foreach (
350
+ array(
351
+ 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-a-role' => 'From a Role',
352
+ 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-a-user' => 'From a User',
353
+ 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-everyone-except-yourself' => 'From Everyone Except You',
354
+ 'how-to-hide-menu-without-preventing-access/' => 'Without Preventing Access',
355
+ )
356
+ as $how_to_url => $how_to_title
357
+ ) {
358
+ printf($how_to_item_template, esc_attr($how_to_url), $how_to_title);
359
+ }
360
+ ?>
361
+ </ul>
362
+ </li>
363
+ <?php
364
+ foreach (
365
+ array(
366
+ 'how-to-give-access-to-menu/' => 'Show a Menu',
367
+ 'how-to-move-and-sort-menus/' => 'Move and Sort Menus',
368
+ 'how-to-add-a-new-menu-item/' => 'Add a New Menu',
369
+ )
370
+ as $how_to_url => $how_to_title
371
+ ) {
372
+ printf($how_to_item_template, esc_attr($how_to_url), $how_to_title);
373
+ }
374
+
375
+ else:
376
+ //Free version tutorials.
377
+ foreach (
378
+ array(
379
+ 'how-to-hide-menus/' => 'Hide a Menu Item',
380
+ 'how-to-hide-menus-cosmetic/' => 'Hide Without Blocking Access',
381
+ 'how-to-add-new-menu/' => 'Add a New Menu',
382
+ )
383
+ as $how_to_url => $how_to_title
384
+ ) {
385
+ printf($how_to_item_template, esc_attr($how_to_url), $how_to_title);
386
+ }
387
+ endif;
388
  ?>
389
  </ul>
390
  </div>
391
  </div>
 
 
 
392
  </div> <!-- / .metabox-holder -->
393
 
394
  <?php
689
 
690
  <?php include dirname(__FILE__) . '/cap-suggestion-box.php'; ?>
691
 
692
+ <?php include dirname(__FILE__) . '/test-access-screen.php'; ?>
693
+
694
  <?php
695
  if ( $is_pro_version ) {
696
  include $extrasDirectory . '/page-dropdown.php';
includes/menu-editor-core.php CHANGED
@@ -17,6 +17,7 @@ require $thisDirectory . '/menu.php';
17
  require $thisDirectory . '/auto-versioning.php';
18
  require $thisDirectory . '/../ajax-wrapper/AjaxWrapper.php';
19
  require $thisDirectory . '/module.php';
 
20
 
21
  class WPMenuEditor extends MenuEd_ShadowPluginFramework {
22
  const WPML_CONTEXT = 'admin-menu-editor menu texts';
@@ -119,6 +120,13 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
119
  */
120
  private $caps_used_in_menu = array();
121
 
 
 
 
 
 
 
 
122
  function init(){
123
  $this->sitewide_options = true;
124
 
@@ -168,6 +176,10 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
168
  //the current site but did not exist on the site where the user last edited the menu configuration.
169
  'unused_item_position' => 'relative', //"relative" or "bottom".
170
 
 
 
 
 
171
  //Verbosity level of menu permission errors.
172
  'error_verbosity' => self::VERBOSITY_NORMAL,
173
 
@@ -293,6 +305,12 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
293
  //Multisite: Clear role and capability caches when switching to another site.
294
  add_action('switch_blog', array($this, 'clear_site_specific_caches'), 10, 0);
295
 
 
 
 
 
 
 
296
  //Utility actions. Modules can use them in their templates.
297
  add_action('admin_menu_editor-display_tabs', array($this, 'display_editor_tabs'));
298
  add_action('admin_menu_editor-display_header', array($this, 'display_settings_page_header'));
@@ -455,7 +473,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
455
  if ( is_network_admin() ) {
456
  $screen_hook_name .= '-network';
457
  }
458
- add_meta_box("ws-ame-screen-options", "[AME placeholder]", '__return_false', $screen_hook_name);
 
 
459
  }
460
 
461
  //Compatibility fix for the WooCommerce order count bubble. Must be run before storing or processing $submenu.
@@ -508,8 +528,17 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
508
  $this->build_custom_wp_menu($this->merged_custom_menu['tree']);
509
  $this->user_cap_cache_enabled = false;
510
 
 
 
 
 
 
511
  if ( !$this->user_can_access_current_page() ) {
512
  $this->log_security_note('DENY access.');
 
 
 
 
513
  $message = 'You do not have sufficient permissions to access this admin page.';
514
 
515
  if ( ($this->options['error_verbosity'] >= self::VERBOSITY_NORMAL) ) {
@@ -532,6 +561,10 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
532
  wp_die($message);
533
  } else {
534
  $this->log_security_note('ALLOW access.');
 
 
 
 
535
  }
536
 
537
  //Replace the admin menu just before it is displayed and restore it afterwards.
@@ -1032,7 +1065,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1032
  return array(
1033
  'user_login' => $user->get('user_login'),
1034
  'id' => $user->ID,
1035
- 'roles' => !empty($user->roles) ? (array)($user->roles) : array(),
1036
  'capabilities' => $this->castValuesToBool($user->caps),
1037
  'meta_capabilities' => array(),
1038
  'display_name' => $user->display_name,
@@ -1228,6 +1261,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1228
  *
1229
  * @param null $config_id
1230
  * @return array|null Either a menu in the internal format, or NULL if there is no custom menu available.
 
1231
  */
1232
  public function load_custom_menu($config_id = null) {
1233
  if ( $config_id === null ) {
@@ -1240,6 +1274,10 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1240
 
1241
  $this->loaded_menu_config_id = $config_id;
1242
 
 
 
 
 
1243
  if ( $config_id === 'network-admin' ) {
1244
  if ( empty($this->options['custom_network_menu']) ) {
1245
  return null;
@@ -1292,6 +1330,14 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1292
  return ($this->options['menu_config_scope'] === 'site');
1293
  }
1294
 
 
 
 
 
 
 
 
 
1295
  /**
1296
  * Determine if the current user may use the menu editor.
1297
  *
@@ -1549,6 +1595,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1549
 
1550
  //Lets merge in the unused items.
1551
  $max_menu_position = !empty($positions_by_template) ? max($positions_by_template) : 100;
 
1552
  foreach ($this->item_templates as $template_id => $template){
1553
  //Skip used menus and separators
1554
  if ( !empty($template['used']) || !empty($template['defaults']['separator'])) {
@@ -1561,6 +1608,8 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1561
  $entry['defaults'] = $template['defaults'];
1562
  $entry['unused'] = true; //Note that this item is unused
1563
 
 
 
1564
  if ($this->options['unused_item_position'] === 'relative') {
1565
 
1566
  //Attempt to maintain relative menu order.
@@ -1677,6 +1726,20 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1677
  $this->page_access_lookup[$item['url']][$priority] = $item['access_level'];
1678
  }
1679
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1680
  /**
1681
  * Generate WP-compatible $menu and $submenu arrays from a custom menu tree.
1682
  *
@@ -1747,6 +1810,10 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1747
  $this->page_access_lookup[$url] = reset($capabilities);
1748
  }
1749
 
 
 
 
 
1750
  //Convert the prepared tree to the internal WordPress format.
1751
  foreach($new_tree as $topmenu) {
1752
  $trueAccess = isset($this->page_access_lookup[$topmenu['url']]) ? $this->page_access_lookup[$topmenu['url']] : null;
@@ -2398,6 +2465,14 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2398
  }
2399
  }
2400
 
 
 
 
 
 
 
 
 
2401
  //How verbose "access denied" errors should be.
2402
  if ( !empty($this->post['error_verbosity']) ) {
2403
  $error_verbosity = intval($this->post['error_verbosity']);
@@ -3466,6 +3541,10 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
3466
  return $log;
3467
  }
3468
 
 
 
 
 
3469
  /**
3470
  * WPML support: Update strings that need translation.
3471
  *
@@ -4050,6 +4129,11 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
4050
  'className' => 'ameAdminCss',
4051
  'title' => 'Admin CSS',
4052
  ),*/
 
 
 
 
 
4053
  'hide-admin-menu' => array(
4054
  'relativePath' => 'extras/modules/hide-admin-menu/hide-admin-menu.php',
4055
  'className' => 'ameAdminMenuHider',
17
  require $thisDirectory . '/auto-versioning.php';
18
  require $thisDirectory . '/../ajax-wrapper/AjaxWrapper.php';
19
  require $thisDirectory . '/module.php';
20
+ require $thisDirectory . '/persistent-module.php';
21
 
22
  class WPMenuEditor extends MenuEd_ShadowPluginFramework {
23
  const WPML_CONTEXT = 'admin-menu-editor menu texts';
120
  */
121
  private $caps_used_in_menu = array();
122
 
123
+ public $is_access_test = false;
124
+ private $test_menu = null;
125
+ /**
126
+ * @var ameAccessTestRunner|null
127
+ */
128
+ private $access_test_runner = null;
129
+
130
  function init(){
131
  $this->sitewide_options = true;
132
 
176
  //the current site but did not exist on the site where the user last edited the menu configuration.
177
  'unused_item_position' => 'relative', //"relative" or "bottom".
178
 
179
+ //Permissions for menu items that are not part of the save menu configuration.
180
+ //The default is to leave the permissions unchanged.
181
+ 'unused_item_permissions' => 'unchanged', //"unchanged" or "match_plugin_access".
182
+
183
  //Verbosity level of menu permission errors.
184
  'error_verbosity' => self::VERBOSITY_NORMAL,
185
 
305
  //Multisite: Clear role and capability caches when switching to another site.
306
  add_action('switch_blog', array($this, 'clear_site_specific_caches'), 10, 0);
307
 
308
+ //"Test Access" feature.
309
+ if ( (defined('DOING_AJAX') && DOING_AJAX) || isset($this->get['ame-test-menu-access-as']) ) {
310
+ require_once 'access-test-runner.php';
311
+ $this->access_test_runner = new ameAccessTestRunner($this, $this->get);
312
+ }
313
+
314
  //Utility actions. Modules can use them in their templates.
315
  add_action('admin_menu_editor-display_tabs', array($this, 'display_editor_tabs'));
316
  add_action('admin_menu_editor-display_header', array($this, 'display_settings_page_header'));
473
  if ( is_network_admin() ) {
474
  $screen_hook_name .= '-network';
475
  }
476
+ if ( $this->current_tab === 'editor' ) {
477
+ add_meta_box("ws-ame-screen-options", "[AME placeholder]", '__return_false', $screen_hook_name);
478
+ }
479
  }
480
 
481
  //Compatibility fix for the WooCommerce order count bubble. Must be run before storing or processing $submenu.
528
  $this->build_custom_wp_menu($this->merged_custom_menu['tree']);
529
  $this->user_cap_cache_enabled = false;
530
 
531
+ if ( $this->is_access_test ) {
532
+ $this->access_test_runner['wasCustomMenuApplied'] = true;
533
+ $this->access_test_runner->setCurrentMenuItem($this->get_current_menu_item());
534
+ }
535
+
536
  if ( !$this->user_can_access_current_page() ) {
537
  $this->log_security_note('DENY access.');
538
+ if ( $this->is_access_test ) {
539
+ $this->access_test_runner['userCanAccessCurrentPage'] = false;
540
+ }
541
+
542
  $message = 'You do not have sufficient permissions to access this admin page.';
543
 
544
  if ( ($this->options['error_verbosity'] >= self::VERBOSITY_NORMAL) ) {
561
  wp_die($message);
562
  } else {
563
  $this->log_security_note('ALLOW access.');
564
+ if ( $this->is_access_test ) {
565
+ $this->access_test_runner['userCanAccessCurrentPage'] =
566
+ ($this->access_test_runner['currentMenuItem'] !== null);
567
+ }
568
  }
569
 
570
  //Replace the admin menu just before it is displayed and restore it afterwards.
1065
  return array(
1066
  'user_login' => $user->get('user_login'),
1067
  'id' => $user->ID,
1068
+ 'roles' => !empty($user->roles) ? array_values((array)($user->roles)) : array(),
1069
  'capabilities' => $this->castValuesToBool($user->caps),
1070
  'meta_capabilities' => array(),
1071
  'display_name' => $user->display_name,
1261
  *
1262
  * @param null $config_id
1263
  * @return array|null Either a menu in the internal format, or NULL if there is no custom menu available.
1264
+ * @throws InvalidMenuException
1265
  */
1266
  public function load_custom_menu($config_id = null) {
1267
  if ( $config_id === null ) {
1274
 
1275
  $this->loaded_menu_config_id = $config_id;
1276
 
1277
+ if ( $this->is_access_test ) {
1278
+ return $this->test_menu;
1279
+ }
1280
+
1281
  if ( $config_id === 'network-admin' ) {
1282
  if ( empty($this->options['custom_network_menu']) ) {
1283
  return null;
1330
  return ($this->options['menu_config_scope'] === 'site');
1331
  }
1332
 
1333
+ function save_options() {
1334
+ if ( $this->is_access_test ) {
1335
+ //Don't change live settings during an access test.
1336
+ return false;
1337
+ }
1338
+ return parent::save_options();
1339
+ }
1340
+
1341
  /**
1342
  * Determine if the current user may use the menu editor.
1343
  *
1595
 
1596
  //Lets merge in the unused items.
1597
  $max_menu_position = !empty($positions_by_template) ? max($positions_by_template) : 100;
1598
+ $new_grant_access = $this->get_new_menu_grant_access();
1599
  foreach ($this->item_templates as $template_id => $template){
1600
  //Skip used menus and separators
1601
  if ( !empty($template['used']) || !empty($template['defaults']['separator'])) {
1608
  $entry['defaults'] = $template['defaults'];
1609
  $entry['unused'] = true; //Note that this item is unused
1610
 
1611
+ $entry['grant_access'] = $new_grant_access;
1612
+
1613
  if ($this->options['unused_item_position'] === 'relative') {
1614
 
1615
  //Attempt to maintain relative menu order.
1726
  $this->page_access_lookup[$item['url']][$priority] = $item['access_level'];
1727
  }
1728
 
1729
+ /**
1730
+ * Get the access settings for menu items that are not part of the saved menu configuration.
1731
+ *
1732
+ * Typically, this applies to new menus that were added by recently activated plugins.
1733
+ *
1734
+ * @return array
1735
+ */
1736
+ public function get_new_menu_grant_access() {
1737
+ if ( $this->options['unused_item_permissions'] === 'unchanged' ) {
1738
+ return array();
1739
+ }
1740
+ return apply_filters('admin_menu_editor-new_menu_grant_access', array());
1741
+ }
1742
+
1743
  /**
1744
  * Generate WP-compatible $menu and $submenu arrays from a custom menu tree.
1745
  *
1810
  $this->page_access_lookup[$url] = reset($capabilities);
1811
  }
1812
 
1813
+ if ( $this->is_access_test ) {
1814
+ $this->access_test_runner->onFinalTreeReady($new_tree);
1815
+ }
1816
+
1817
  //Convert the prepared tree to the internal WordPress format.
1818
  foreach($new_tree as $topmenu) {
1819
  $trueAccess = isset($this->page_access_lookup[$topmenu['url']]) ? $this->page_access_lookup[$topmenu['url']] : null;
2465
  }
2466
  }
2467
 
2468
+ //Permissions for unused menu items.
2469
+ if (
2470
+ isset($this->post['unused_item_permissions'])
2471
+ && in_array($this->post['unused_item_permissions'], array('unchanged', 'match_plugin_access'), true)
2472
+ ) {
2473
+ $this->options['unused_item_permissions'] = strval($this->post['unused_item_permissions']);
2474
+ }
2475
+
2476
  //How verbose "access denied" errors should be.
2477
  if ( !empty($this->post['error_verbosity']) ) {
2478
  $error_verbosity = intval($this->post['error_verbosity']);
3541
  return $log;
3542
  }
3543
 
3544
+ public function get_security_log() {
3545
+ return $this->security_log;
3546
+ }
3547
+
3548
  /**
3549
  * WPML support: Update strings that need translation.
3550
  *
4129
  'className' => 'ameAdminCss',
4130
  'title' => 'Admin CSS',
4131
  ),*/
4132
+ /*'tweaks' => array(
4133
+ 'relativePath' => 'modules/tweaks/tweaks.php',
4134
+ 'className' => 'ameTweakManager',
4135
+ 'title' => 'Tweaks',
4136
+ ),*/
4137
  'hide-admin-menu' => array(
4138
  'relativePath' => 'extras/modules/hide-admin-menu/hide-admin-menu.php',
4139
  'className' => 'ameAdminMenuHider',
includes/module.php CHANGED
@@ -131,4 +131,20 @@ abstract class ameModule {
131
  public function handleSettingsForm($post = array()) {
132
  //Override this method to process a form submitted from the module's tab.
133
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
131
  public function handleSettingsForm($post = array()) {
132
  //Override this method to process a form submitted from the module's tab.
133
  }
134
+
135
+ protected function getScopedOption($name, $defaultValue = null) {
136
+ if ( $this->menuEditor->get_plugin_option('menu_config_scope') === 'site' ) {
137
+ return get_option($name, $defaultValue);
138
+ } else {
139
+ return get_site_option($name, $defaultValue);
140
+ }
141
+ }
142
+
143
+ protected function setScopedOption($name, $value, $autoload = null) {
144
+ if ( $this->menuEditor->get_plugin_option('menu_config_scope') === 'site' ) {
145
+ update_option($name, $value, $autoload);
146
+ } else {
147
+ WPMenuEditor::atomic_update_site_option($name, $value);
148
+ }
149
+ }
150
  }
includes/persistent-module.php ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ abstract class amePersistentModule extends ameModule {
4
+ /**
5
+ * @var string Database option where module settings are stored.
6
+ */
7
+ protected $optionName = '';
8
+
9
+ /**
10
+ * @var array|null Module settings. NULL when settings haven't been loaded yet.
11
+ */
12
+ protected $settings = null;
13
+
14
+ /**
15
+ * @var array Default module settings.
16
+ */
17
+ protected $defaultSettings = array();
18
+
19
+ public function __construct($menuEditor) {
20
+ if ( $this->optionName === '' ) {
21
+ throw new LogicException(__CLASS__ . '::$optionName is an empty string. You must set it to a valid option name.');
22
+ }
23
+
24
+ parent::__construct($menuEditor);
25
+ }
26
+
27
+ public function loadSettings() {
28
+ if ( isset($this->settings) ) {
29
+ return $this->settings;
30
+ }
31
+
32
+ $json = $this->getScopedOption($this->optionName, null);
33
+ if ( is_string($json) && !empty($json) ) {
34
+ $settings = json_decode($json, true);
35
+ } else {
36
+ $settings = array();
37
+ }
38
+
39
+ $this->settings = array_merge($this->defaultSettings, $settings);
40
+
41
+ return $this->settings;
42
+ }
43
+
44
+ public function saveSettings() {
45
+ $settings = json_encode($this->settings);
46
+ //Save per site or site-wide based on plugin configuration.
47
+ $this->setScopedOption($this->optionName, $settings);
48
+ }
49
+
50
+ protected function getTemplateVariables($templateName) {
51
+ $variables = parent::getTemplateVariables($templateName);
52
+ if ( $templateName === $this->moduleId ) {
53
+ $variables = array_merge(
54
+ $variables,
55
+ array(
56
+ 'settings' => $this->loadSettings(),
57
+ )
58
+ );
59
+ }
60
+ return $variables;
61
+ }
62
+ }
includes/settings-page.php CHANGED
@@ -117,6 +117,8 @@ $isProVersion = apply_filters('admin_menu_editor_is_pro', false);
117
  </td>
118
  </tr>
119
 
 
 
120
  <tr>
121
  <th scope="row">
122
  Modules
@@ -261,6 +263,51 @@ $isProVersion = apply_filters('admin_menu_editor_is_pro', false);
261
  </fieldset>
262
  </td>
263
  </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  <?php endif; ?>
265
 
266
  <tr>
117
  </td>
118
  </tr>
119
 
120
+ <?php do_action('admin-menu-editor-display_addons'); ?>
121
+
122
  <tr>
123
  <th scope="row">
124
  Modules
263
  </fieldset>
264
  </td>
265
  </tr>
266
+
267
+ <tr>
268
+ <th scope="row">
269
+ New menu visibility
270
+ <a class="ws_tooltip_trigger"
271
+ title="This setting controls the default permissions of menu items that are
272
+ not present in the last saved menu configuration.
273
+ &lt;br&gt;&lt;br&gt;
274
+ This includes new menus added by plugins and themes.
275
+ In Multisite, it also applies to menus that exist on some sites but not others.
276
+ It doesn't affect menu items that you add through the Admin Menu Editor interface.">
277
+ <div class="dashicons dashicons-info"></div>
278
+ </a>
279
+ </th>
280
+ <td>
281
+ <fieldset>
282
+ <p>
283
+ <label>
284
+ <input type="radio" name="unused_item_permissions" value="unchanged"
285
+ <?php checked('unchanged', $settings['unused_item_permissions']); ?>>
286
+ Leave unchanged (default)
287
+
288
+ <br><span class="description">
289
+ No special restrictions. Visibility will depend on the plugin
290
+ that added the menus.
291
+ </span>
292
+ </label>
293
+ </p>
294
+
295
+ <p>
296
+ <label>
297
+ <input type="radio" name="unused_item_permissions" value="match_plugin_access"
298
+ <?php checked('match_plugin_access', $settings['unused_item_permissions']); ?>>
299
+ Show only to users who can access this plugin
300
+
301
+ <br><span class="description">
302
+ Automatically hides all new and unrecognized menus from regular users.
303
+ To make new menus visible, you have to manually enable them in the menu editor.
304
+ </span>
305
+ </label>
306
+ </p>
307
+
308
+ </fieldset>
309
+ </td>
310
+ </tr>
311
  <?php endif; ?>
312
 
313
  <tr>
includes/shadow_plugin_framework.php CHANGED
@@ -58,8 +58,8 @@ class MenuEd_ShadowPluginFramework {
58
  /************************************
59
  Add the default hooks
60
  ************************************/
61
- add_action('activate_'.$this->plugin_basename, array(&$this,'activate'));
62
- add_action('deactivate_'.$this->plugin_basename, array(&$this,'deactivate'));
63
 
64
  $this->init(); //Call the plugin's init() function
65
  $this->init_finish(); //Complete initialization by loading settings, etc
@@ -97,7 +97,7 @@ class MenuEd_ShadowPluginFramework {
97
 
98
  //Add a "Settings" action link
99
  if ($this->settings_link)
100
- add_filter('plugin_action_links', array(&$this, 'plugin_action_links'), 10, 2);
101
 
102
  if ($this->magic_hooks)
103
  $this->set_magic_hooks();
@@ -240,7 +240,7 @@ class MenuEd_ShadowPluginFramework {
240
  //Get the hook's tag from the method name
241
  $hook = substr($method->name, 5);
242
  //Add the hook. Uses add_filter because add_action is simply a wrapper of the same.
243
- add_filter($hook, array(&$this, $method->name),
244
  $this->get_magic_hook_priority(), $method->getNumberOfParameters());
245
  }
246
  }
@@ -282,8 +282,9 @@ class MenuEd_ShadowPluginFramework {
282
  * @return array
283
  */
284
  function plugin_action_links($links, $file) {
285
- if ($file == $this->plugin_basename)
286
- $links[] = "<a href='" . $this->settings_link . "'>" . __('Settings') . "</a>";
 
287
  return $links;
288
  }
289
 
58
  /************************************
59
  Add the default hooks
60
  ************************************/
61
+ add_action('activate_'.$this->plugin_basename, array($this,'activate'));
62
+ add_action('deactivate_'.$this->plugin_basename, array($this,'deactivate'));
63
 
64
  $this->init(); //Call the plugin's init() function
65
  $this->init_finish(); //Complete initialization by loading settings, etc
97
 
98
  //Add a "Settings" action link
99
  if ($this->settings_link)
100
+ add_filter('plugin_action_links', array($this, 'plugin_action_links'), 10, 2);
101
 
102
  if ($this->magic_hooks)
103
  $this->set_magic_hooks();
240
  //Get the hook's tag from the method name
241
  $hook = substr($method->name, 5);
242
  //Add the hook. Uses add_filter because add_action is simply a wrapper of the same.
243
+ add_filter($hook, array($this, $method->name),
244
  $this->get_magic_hook_priority(), $method->getNumberOfParameters());
245
  }
246
  }
282
  * @return array
283
  */
284
  function plugin_action_links($links, $file) {
285
+ if (($file == $this->plugin_basename) && is_array($links)) {
286
+ $links[] = "<a href='" . $this->settings_link . "'>" . __('Settings') . "</a>";
287
+ }
288
  return $links;
289
  }
290
 
includes/test-access-screen.php ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="ws_ame_test_access_screen">
2
+ <div id="ws_ame_test_inputs">
3
+ <label class="ws_ame_test_input">
4
+ <span class="ws_ame_test_input_name">Menu item</span>
5
+ <select name="ws_ame_test_menu_item" id="ws_ame_test_menu_item" class="ws_ame_test_input_value"></select>
6
+ </label>
7
+
8
+ <label class="ws_ame_test_input">
9
+ <span class="ws_ame_test_input_name">Log in as user</span>
10
+ <input type="text" class="ws_ame_test_input_value" id="ws_ame_test_access_username">
11
+ </label>
12
+
13
+ <label class="ws_ame_test_input">
14
+ <span class="ws_ame_test_input_name">Relevant role (optional)</span>
15
+ <select name="ws_ame_test_relevant_actor" id="ws_ame_test_relevant_actor" class="ws_ame_test_input_value"></select>
16
+ </label>
17
+
18
+ <div class="ws_ame_test_input">
19
+ <span class="ws_ame_test_input_name">What you want to happen</span>
20
+ <fieldset class="ws_ame_test_input_value">
21
+ <label>
22
+ <input type="radio" name="ws_ame_desired_test_outcome" value="visible" checked>
23
+ Menu is <strong>visible</strong>
24
+ </label><br>
25
+ <label>
26
+ <input type="radio" name="ws_ame_desired_test_outcome" value="hidden">
27
+ Menu is <strong>hidden</strong>
28
+ </label><br>
29
+ </fieldset>
30
+ </div>
31
+
32
+ <div id="ws_ame_test_actions">
33
+ <div id="ws_ame_test_button_container">
34
+ <input type="button" class="button-primary" value="Start Test" id="ws_ame_start_access_test">
35
+ </div>
36
+ <div id="ws_ame_test_progress">
37
+ <span class="spinner is-active"></span>
38
+ <span id="ws_ame_test_progress_text">
39
+ Test hasn't started yet.
40
+ </span>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="clear"></div>
45
+ </div>
46
+
47
+ <div id="ws_ame_test_access_body">
48
+ <div id="ws_ame_test_frame_container">
49
+ <span id="ws_ame_test_frame_placeholder">
50
+ <em>Test page will appear here.</em><br>
51
+ </span>
52
+ <iframe src="" frameborder="0" sandbox="allow-scripts" id="ws_ame_test_access_frame"></iframe>
53
+ </div>
54
+
55
+ <div id="ws_ame_test_access_sidebar">
56
+ <span id="ws_ame_test_output_placeholder">
57
+ <em>Analysis will appear here.</em>
58
+ </span>
59
+ <div id="ws_ame_test_output">
60
+ <h4>Result</h4>
61
+
62
+ <h4>Analysis</h4>
63
+ <h4>Suggestions</h4>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
js/menu-editor.js CHANGED
@@ -46,6 +46,9 @@
46
  * @property {string|null} wsEditorData.selectedMenu
47
  * @property {string|null} wsEditorData.selectedSubmenu
48
  *
 
 
 
49
  * @property {boolean} wsEditorData.isDemoMode
50
  * @property {boolean} wsEditorData.isMasterMode
51
  */
@@ -774,6 +777,15 @@ var knownMenuFields = {
774
  }
775
  }),
776
 
 
 
 
 
 
 
 
 
 
777
  'icon_url' : $.extend({}, baseField, {
778
  caption: 'Icon URL',
779
  type : 'icon_selector',
@@ -834,10 +846,50 @@ var knownMenuFields = {
834
  }
835
  }),
836
 
837
- 'css_class' : $.extend({}, baseField, {
838
- caption: 'CSS classes',
 
 
 
 
839
  advanced : true,
840
- onlyForTopMenus: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
841
  }),
842
 
843
  'open_in' : $.extend({}, baseField, {
@@ -888,48 +940,24 @@ var knownMenuFields = {
888
  }
889
  }),
890
 
891
- 'colors' : $.extend({}, baseField, {
892
- caption: 'Color scheme',
893
- defaultValue: 'Default',
894
- type: 'color_scheme_editor',
895
- onlyForTopMenus: true,
896
- visible: false,
897
  advanced : true,
 
 
898
 
899
- display: function(menuItem, displayValue, input, containerNode) {
900
- var colors = getFieldValue(menuItem, 'colors', {}) || {};
901
- var colorList = containerNode.find('.ws_color_scheme_display');
902
-
903
- colorList.empty();
904
- var count = 0, maxColorsToShow = 7;
905
-
906
- $.each(colors, function(name, value) {
907
- if ( !value || (count >= maxColorsToShow) ) {
908
- return;
909
- }
910
-
911
- colorList.append(
912
- $('<span></span>').addClass('ws_color_display_item').css('background-color', value)
913
- );
914
- count++;
915
- });
916
-
917
- if (count === 0) {
918
- colorList.append('Default');
919
- }
920
-
921
- return 'Placeholder. You should never see this.';
922
- },
923
-
924
- write: function(menuItem) {
925
- //Menu colors can't be directly edited.
926
- }
927
  }),
928
 
929
- 'page_title' : $.extend({}, baseField, {
930
- caption: "Window title",
931
- standardCaption : true,
932
- advanced : true
 
 
933
  }),
934
 
935
  'page_heading' : $.extend({}, baseField, {
@@ -939,14 +967,14 @@ var knownMenuFields = {
939
  visible: false
940
  }),
941
 
942
- 'hookname' : $.extend({}, baseField, {
943
- caption: 'Hook name',
944
- advanced : true,
945
- onlyForTopMenus: true
946
  }),
947
 
948
  'is_always_open' : $.extend({}, baseField, {
949
- caption: 'Keep this menu open',
950
  advanced : true,
951
  onlyForTopMenus: true,
952
  type: 'checkbox',
@@ -1054,6 +1082,10 @@ function buildEditboxField(entry, field_name, field_settings){
1054
  .add('<input type="button" class="button ws_open_color_editor" value="Edit...">');
1055
  break;
1056
 
 
 
 
 
1057
  case 'text':
1058
  /* falls through */
1059
  default:
@@ -1068,6 +1100,9 @@ function buildEditboxField(entry, field_name, field_settings){
1068
  if (!field_settings.standardCaption) {
1069
  className += ' ws_no_field_caption';
1070
  }
 
 
 
1071
 
1072
  var caption = '';
1073
  if (field_settings.standardCaption) {
@@ -1094,7 +1129,7 @@ function buildEditboxField(entry, field_name, field_settings){
1094
 
1095
  editField
1096
  .append(
1097
- $('<img class="ws_reset_button" title="Reset to default value">')
1098
  .attr('src', wsEditorData.imagesUrl + '/transparent16.png')
1099
  ).data('field_name', field_name);
1100
 
@@ -1725,6 +1760,10 @@ function readAllFields(container){
1725
  if (field_name === 'embedded_page_id') {
1726
  return true;
1727
  }
 
 
 
 
1728
 
1729
  //Find the field (usually an input or select element).
1730
  var input_box = field.find('.ws_field_value');
@@ -1997,6 +2036,8 @@ function ameOnDomReady() {
1997
  knownMenuFields.access_level.visible = true;
1998
  knownMenuFields.page_heading.visible = true;
1999
  knownMenuFields.colors.visible = true;
 
 
2000
  knownMenuFields.extra_capability.visible = false; //Superseded by the "access_level" field.
2001
 
2002
  //The Pro version supports submenu icons, but they can be disabled by the user.
@@ -4787,6 +4828,180 @@ function ameOnDomReady() {
4787
  }
4788
  }
4789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4790
  //Finally, show the menu
4791
  loadMenuConfiguration(customMenu);
4792
 
46
  * @property {string|null} wsEditorData.selectedMenu
47
  * @property {string|null} wsEditorData.selectedSubmenu
48
  *
49
+ * @property {string} wsEditorData.setTestConfigurationNonce
50
+ * @property {string} wsEditorData.testAccessNonce
51
+ *
52
  * @property {boolean} wsEditorData.isDemoMode
53
  * @property {boolean} wsEditorData.isMasterMode
54
  */
777
  }
778
  }),
779
 
780
+ 'appearance_heading' : $.extend({}, baseField, {
781
+ caption: 'Appearance',
782
+ advanced : true,
783
+ onlyForTopMenus: false,
784
+ type: 'heading',
785
+ standardCaption: false,
786
+ visible: false //Only visible in the Pro version.
787
+ }),
788
+
789
  'icon_url' : $.extend({}, baseField, {
790
  caption: 'Icon URL',
791
  type : 'icon_selector',
846
  }
847
  }),
848
 
849
+ 'colors' : $.extend({}, baseField, {
850
+ caption: 'Color scheme',
851
+ defaultValue: 'Default',
852
+ type: 'color_scheme_editor',
853
+ onlyForTopMenus: true,
854
+ visible: false,
855
  advanced : true,
856
+
857
+ display: function(menuItem, displayValue, input, containerNode) {
858
+ var colors = getFieldValue(menuItem, 'colors', {}) || {};
859
+ var colorList = containerNode.find('.ws_color_scheme_display');
860
+
861
+ colorList.empty();
862
+ var count = 0, maxColorsToShow = 7;
863
+
864
+ $.each(colors, function(name, value) {
865
+ if ( !value || (count >= maxColorsToShow) ) {
866
+ return;
867
+ }
868
+
869
+ colorList.append(
870
+ $('<span></span>').addClass('ws_color_display_item').css('background-color', value)
871
+ );
872
+ count++;
873
+ });
874
+
875
+ if (count === 0) {
876
+ colorList.append('Default');
877
+ }
878
+
879
+ return 'Placeholder. You should never see this.';
880
+ },
881
+
882
+ write: function(menuItem) {
883
+ //Menu colors can't be directly edited.
884
+ }
885
+ }),
886
+
887
+ 'html_heading' : $.extend({}, baseField, {
888
+ caption: 'HTML',
889
+ advanced : true,
890
+ onlyForTopMenus: true,
891
+ type: 'heading',
892
+ standardCaption: false
893
  }),
894
 
895
  'open_in' : $.extend({}, baseField, {
940
  }
941
  }),
942
 
943
+ 'css_class' : $.extend({}, baseField, {
944
+ caption: 'CSS classes',
 
 
 
 
945
  advanced : true,
946
+ onlyForTopMenus: true
947
+ }),
948
 
949
+ 'hookname' : $.extend({}, baseField, {
950
+ caption: 'ID attribute',
951
+ advanced : true,
952
+ onlyForTopMenus: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
953
  }),
954
 
955
+ 'page_properties_heading' : $.extend({}, baseField, {
956
+ caption: 'Page',
957
+ advanced : true,
958
+ onlyForTopMenus: true,
959
+ type: 'heading',
960
+ standardCaption: false
961
  }),
962
 
963
  'page_heading' : $.extend({}, baseField, {
967
  visible: false
968
  }),
969
 
970
+ 'page_title' : $.extend({}, baseField, {
971
+ caption: "Window title",
972
+ standardCaption : true,
973
+ advanced : true
974
  }),
975
 
976
  'is_always_open' : $.extend({}, baseField, {
977
+ caption: 'Keep this menu expanded',
978
  advanced : true,
979
  onlyForTopMenus: true,
980
  type: 'checkbox',
1082
  .add('<input type="button" class="button ws_open_color_editor" value="Edit...">');
1083
  break;
1084
 
1085
+ case 'heading':
1086
+ inputBox = $('<span>' + field_settings.caption + '</span>');
1087
+ break;
1088
+
1089
  case 'text':
1090
  /* falls through */
1091
  default:
1100
  if (!field_settings.standardCaption) {
1101
  className += ' ws_no_field_caption';
1102
  }
1103
+ if (field_settings.type === 'heading') {
1104
+ className += ' ws_field_group_heading';
1105
+ }
1106
 
1107
  var caption = '';
1108
  if (field_settings.standardCaption) {
1129
 
1130
  editField
1131
  .append(
1132
+ $('<img class="ws_reset_button" title="Reset to default value" src="">')
1133
  .attr('src', wsEditorData.imagesUrl + '/transparent16.png')
1134
  ).data('field_name', field_name);
1135
 
1760
  if (field_name === 'embedded_page_id') {
1761
  return true;
1762
  }
1763
+ //Headings contain no useful data.
1764
+ if (field.hasClass('ws_field_group_heading')) {
1765
+ return true;
1766
+ }
1767
 
1768
  //Find the field (usually an input or select element).
1769
  var input_box = field.find('.ws_field_value');
2036
  knownMenuFields.access_level.visible = true;
2037
  knownMenuFields.page_heading.visible = true;
2038
  knownMenuFields.colors.visible = true;
2039
+ knownMenuFields.appearance_heading.visible = true;
2040
+ knownMenuFields.appearance_heading.onlyForTopMenus = false;
2041
  knownMenuFields.extra_capability.visible = false; //Superseded by the "access_level" field.
2042
 
2043
  //The Pro version supports submenu icons, but they can be disabled by the user.
4828
  }
4829
  }
4830
 
4831
+ /******************************************************************
4832
+ "Test Access" feature
4833
+ ******************************************************************/
4834
+ var testAccessDialog = $('#ws_ame_test_access_screen').dialog({
4835
+ autoOpen: false,
4836
+ modal: true,
4837
+ closeText: ' ',
4838
+ title: 'Test access',
4839
+ width: 900
4840
+ //draggable: false
4841
+ }),
4842
+ testMenuItemList = $('#ws_ame_test_menu_item'),
4843
+ testActorList = $('#ws_ame_test_relevant_actor'),
4844
+ testAccessButton = $('#ws_ame_start_access_test'),
4845
+ testAccessFrame = $('#ws_ame_test_access_frame'),
4846
+ testConfig = null,
4847
+
4848
+ testProgress = $('#ws_ame_test_progress'),
4849
+ testProgressText = $('#ws_ame_test_progress_text');
4850
+
4851
+ $('#ws_test_access').click(function () {
4852
+ testConfig = readMenuTreeState();
4853
+
4854
+ var selectedMenuContainer = getSelectedMenu(),
4855
+ selectedItemContainer = getSelectedSubmenuItem(),
4856
+ selectedMenu = null,
4857
+ selectedItem = null,
4858
+ selectedUrl = null;
4859
+ if (selectedMenuContainer.length > 0) {
4860
+ selectedMenu = selectedMenuContainer.data('menu_item');
4861
+ selectedUrl = getFieldValue(selectedMenu, 'url');
4862
+ }
4863
+ if (selectedItemContainer.length > 0) {
4864
+ selectedItem = selectedItemContainer.data('menu_item');
4865
+ selectedUrl = getFieldValue(selectedItem, 'url');
4866
+ }
4867
+
4868
+ function addMenuItems(collection, parentTitle, parentFile) {
4869
+ _.each(collection, function (menuItem) {
4870
+ if (menuItem.separator) {
4871
+ return;
4872
+ }
4873
+
4874
+ var title = formatMenuTitle(getFieldValue(menuItem, 'menu_title', '[Untitled menu]'));
4875
+ if (parentTitle) {
4876
+ title = parentTitle + ' -> ' + title;
4877
+ }
4878
+ var url = getFieldValue(menuItem, 'url', '[no-url]');
4879
+
4880
+ var option = $(
4881
+ '<option>', {
4882
+ val: url,
4883
+ text: title
4884
+ }
4885
+ );
4886
+ option.data('menu_item', menuItem);
4887
+ option.data('parent_file', parentFile || '');
4888
+ option.prop('selected', (url === selectedUrl));
4889
+
4890
+ testMenuItemList.append(option);
4891
+
4892
+ if (menuItem.items) {
4893
+ addMenuItems(menuItem.items, title, getFieldValue(menuItem, 'file', ''));
4894
+ }
4895
+ });
4896
+ }
4897
+
4898
+ //Populate the list of menu items.
4899
+ testMenuItemList.empty();
4900
+ addMenuItems(testConfig.tree);
4901
+
4902
+ //Populate the actor list.
4903
+ testActorList.empty();
4904
+ testActorList.append($('<option>', {text: 'Not selected', val: ''}));
4905
+ _.each(actorSelectorWidget.getVisibleActors(), function (actor) {
4906
+ //TODO: Skip anything that isn't a role
4907
+ var option = $('<option>', {
4908
+ val: actor.id,
4909
+ text: actorSelectorWidget.getNiceName(actor)
4910
+ });
4911
+ testActorList.append(option);
4912
+ });
4913
+
4914
+ //Pre-select the current actor.
4915
+ if (actorSelectorWidget.selectedActor !== null) {
4916
+ testActorList.val(actorSelectorWidget.selectedActor);
4917
+ }
4918
+
4919
+ testAccessDialog.dialog('open');
4920
+ });
4921
+
4922
+ testAccessButton.click(function () {
4923
+ testAccessButton.prop('disabled', true);
4924
+ testProgress.show();
4925
+ testProgressText.text('Sending menu settings...');
4926
+
4927
+ var selectedOption = testMenuItemList.find('option:selected').first(),
4928
+ selectedMenu = selectedOption.data('menu_item'),
4929
+ menuUrl = selectedOption.val();
4930
+
4931
+ $.ajax(
4932
+ wsEditorData.adminAjaxUrl,
4933
+ {
4934
+ data: {
4935
+ 'action': 'ws_ame_set_test_configuration',
4936
+ 'data': encodeMenuAsJSON(testConfig),
4937
+ '_ajax_nonce': wsEditorData.setTestConfigurationNonce
4938
+ },
4939
+ method: 'post',
4940
+ dataType: 'json',
4941
+ success: function(response, textStatus) {
4942
+ if (!response) {
4943
+ alert('Error: Could not parse the server response.');
4944
+ testAccessButton.prop('disabled', false);
4945
+ return;
4946
+ }
4947
+ if (response.error) {
4948
+ alert(response.error);
4949
+ testAccessButton.prop('disabled', false);
4950
+ return;
4951
+ }
4952
+ if (!response.success) {
4953
+ alert('Error: The request failed, but there is no error information available.');
4954
+ testAccessButton.prop('disabled', false);
4955
+ return;
4956
+ }
4957
+
4958
+ //Caution: Won't work in IE. Needs compat checks.
4959
+ var testPageUrl = new URL(menuUrl, window.location.href);
4960
+ testPageUrl.searchParams.append('ame-test-menu-access-as', $('#ws_ame_test_access_username').val());
4961
+ testPageUrl.searchParams.append('_wpnonce', wsEditorData.testAccessNonce);
4962
+ testPageUrl.searchParams.append('ame-test-relevant-role', testActorList.val());
4963
+
4964
+ testPageUrl.searchParams.append('ame-test-target-item', getFieldValue(selectedMenu, 'file', ''));
4965
+ testPageUrl.searchParams.append('ame-test-target-parent', selectedOption.data('parent_file'));
4966
+
4967
+ testProgressText.text('Loading the test page....');
4968
+ $('#ws_ame_test_frame_placeholder').hide();
4969
+
4970
+ $(window).on('message', receiveTestAccessResults);
4971
+ testAccessFrame
4972
+ .show()
4973
+ .on('load', onAccessTestLoaded)
4974
+ .prop('src', testPageUrl.href);
4975
+ },
4976
+ error: function(jqXHR, textStatus) {
4977
+ alert('HTTP Error: ' + textStatus);
4978
+ testAccessButton.prop('disabled', false);
4979
+ }
4980
+ }
4981
+ );
4982
+ });
4983
+
4984
+ function onAccessTestLoaded() {
4985
+ testAccessFrame.off('load', onAccessTestLoaded);
4986
+ testProgress.hide();
4987
+
4988
+ testAccessButton.prop('disabled', false);
4989
+ }
4990
+
4991
+ function receiveTestAccessResults(event) {
4992
+ if (event.originalEvent.source !== testAccessFrame.get(0).contentWindow) {
4993
+ if (console && console.warn) {
4994
+ console.warn('AME: Received a message from an unexpected source. Message ignored.');
4995
+ }
4996
+ return;
4997
+ }
4998
+ var message = event.originalEvent.data || event.originalEvent.message;
4999
+ console.log('message received', message);
5000
+
5001
+ $(window).off('message', receiveTestAccessResults);
5002
+ }
5003
+
5004
+
5005
  //Finally, show the menu
5006
  loadMenuConfiguration(customMenu);
5007
 
menu-editor.php CHANGED
@@ -3,7 +3,7 @@
3
  Plugin Name: Admin Menu Editor
4
  Plugin URI: http://w-shadow.com/blog/2008/12/20/admin-menu-editor-for-wordpress/
5
  Description: Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
6
- Version: 1.8.2
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
3
  Plugin Name: Admin Menu Editor
4
  Plugin URI: http://w-shadow.com/blog/2008/12/20/admin-menu-editor-for-wordpress/
5
  Description: Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
6
+ Version: 1.8.3
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
modules/tweaks/default-tweaks.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ return array(
3
+ 'hide-screen-meta-links' => array(
4
+ 'label' => 'Hide screen meta links',
5
+ 'selector' => '#screen-meta-links'
6
+ ),
7
+ 'hide-screen-options' => array(
8
+ 'label' => 'Hide the "Screen Options" button',
9
+ 'selector' => '#screen-options-link-wrap',
10
+ 'parent' => 'hide-screen-meta-links',
11
+ ),
12
+ 'hide-help-panel' => array(
13
+ 'label' => 'Hide the "Help" button',
14
+ 'selector' => '#contextual-help-link-wrap',
15
+ 'parent' => 'hide-screen-meta-links',
16
+ ),
17
+ 'hide-all-admin-notices' => array(
18
+ 'label' => 'Hide ALL admin notices',
19
+ 'selector' => '.wrap .notice, .wrap .updated',
20
+ ),
21
+ );
modules/tweaks/tweaks-template.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var string $tabUrl Fully qualified URL of the tab.
4
+ * @var array $tweaks
5
+ */
6
+
7
+ ?>
8
+ <div id="ame-tweak-manager">
9
+ <?php require AME_ROOT_DIR . '/modules/actor-selector/actor-selector-template.php'; ?>
10
+
11
+ <pre><?php print_r($tweaks); ?></pre>
12
+ </div>
13
+
modules/tweaks/tweaks.php ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * Idea: Show tweaks as options in menu properties, e.g. in a "Tweaks" section styled like the collapsible
5
+ * property sheets in Delphi.
6
+ */
7
+
8
+ class ameTweakManager extends amePersistentModule {
9
+ protected $tabSlug = 'tweaks';
10
+ protected $tabTitle = 'Tweaks';
11
+ protected $optionName = 'ws_ame_tweak_settings';
12
+
13
+ private $tweaks = array();
14
+
15
+ private $postponedTweaks = array();
16
+ private $pendingSelectorTweaks = array();
17
+
18
+ public function __construct($menuEditor) {
19
+ parent::__construct($menuEditor);
20
+
21
+ add_action('init', array($this, 'processTweaks'), 200);
22
+ add_action('admin_head', array($this, 'outputSelectors'));
23
+ }
24
+
25
+ private function registerTweaks() {
26
+ $this->tweaks = require (__DIR__ . '/default-tweaks.php');
27
+ do_action('admin-menu-editor-register_tweaks', $this);
28
+ }
29
+
30
+ public function processTweaks() {
31
+ $settings = $this->loadSettings();
32
+ $isTweakEnabled = ameUtils::get($settings, 'isTweakEnabled');
33
+
34
+ $this->registerTweaks();
35
+
36
+ $currentUser = wp_get_current_user();
37
+ $roles = $this->menuEditor->get_user_roles($currentUser);
38
+ $isSuperAdmin = is_multisite() && is_super_admin($currentUser->ID);
39
+
40
+ foreach ($this->tweaks as $id => $tweak) {
41
+ if ( empty($isTweakEnabled[$id]) ) {
42
+ continue; //This tweak is not enabled for anyone.
43
+ }
44
+
45
+ if ( !$this->appliesToUser($isTweakEnabled[$id], $currentUser, $roles, $isSuperAdmin) ) {
46
+ continue;
47
+ }
48
+
49
+ if ( isset($tweak['initFilter']) && !call_user_func($tweak['initFilter']) ) {
50
+ continue;
51
+ }
52
+
53
+ if ( !empty($tweak['screens']) || !empty($tweak['screenFilter']) ) {
54
+ $this->postponedTweaks[$id] = $tweak;
55
+ continue;
56
+ }
57
+
58
+ $this->applyTweak($id, $tweak);
59
+ }
60
+
61
+ if ( !empty($this->postponedTweaks) ) {
62
+ add_action('current_screen', array($this, 'processPostponedTweaks'), 10, 1);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * @param array $enabledForActor
68
+ * @param WP_User $user
69
+ * @param array $roles
70
+ * @param bool $isSuperAdmin
71
+ * @return bool
72
+ */
73
+ private function appliesToUser($enabledForActor, $user, $roles, $isSuperAdmin = false) {
74
+ //User-specific settings have priority over everything else.
75
+ $userActor = 'user:' . $user->user_login;
76
+ if ( isset($enabledForActor[$userActor]) ) {
77
+ return $enabledForActor[$userActor];
78
+ }
79
+
80
+ //The "Super Admin" flag has priority over regular roles.
81
+ if ( $isSuperAdmin && isset($enabledForActor['special:super_admin']) ) {
82
+ return $enabledForActor['special:super_admin'];
83
+ }
84
+
85
+ //If it's enabled for any role, it's enabled for the user.
86
+ foreach($roles as $role) {
87
+ if ( !empty($enabledForActor['role:' . $role]) ) {
88
+ return true;
89
+ }
90
+ }
91
+
92
+ //By default, all tweaks are disabled.
93
+ return false;
94
+ }
95
+
96
+ private function applyTweak($id, $tweak) {
97
+ //Run callbacks immediately.
98
+ if ( isset($tweak['callback']) ) {
99
+ call_user_func($tweak['callback']);
100
+ }
101
+
102
+ //Queue selectors for later.
103
+ if ( !empty($tweak['selector']) ) {
104
+ $this->pendingSelectorTweaks[$id] = $tweak;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param WP_Screen $screen
110
+ */
111
+ public function processPostponedTweaks($screen = null) {
112
+ if ( empty($screen) && function_exists('get_current_screen') ) {
113
+ $screen = get_current_screen();
114
+ }
115
+ $screenId = isset($screen, $screen->id) ? $screen->id : null;
116
+
117
+ foreach($this->postponedTweaks as $id => $tweak) {
118
+ if ( !empty($tweak['screens']) && !in_array($screenId, $tweak['screens']) ) {
119
+ continue;
120
+ }
121
+
122
+ if ( !empty($tweak['screenFilter']) && !call_user_func($tweak['screenFilter'], $screen) ) {
123
+ continue;
124
+ }
125
+
126
+ $this->applyTweak($id, $tweak);
127
+ }
128
+
129
+ $this->postponedTweaks = array();
130
+ }
131
+
132
+ public function outputSelectors() {
133
+ if ( empty($this->pendingSelectorTweaks) ) {
134
+ return;
135
+ }
136
+
137
+ $selectors = array();
138
+ foreach($this->pendingSelectorTweaks as $tweak) {
139
+ $selectors[] = $tweak['selector'];
140
+ }
141
+ $css = sprintf(
142
+ '<style type="text/css">%s { display: none; }</style>',
143
+ implode(',', $selectors)
144
+ );
145
+
146
+ echo '<!-- AME selector tweaks -->', "\n", $css, "\n";
147
+
148
+ $this->pendingSelectorTweaks = array();
149
+ }
150
+
151
+ protected function getTemplateVariables($templateName) {
152
+ $variables = parent::getTemplateVariables($templateName);
153
+ $variables['tweaks'] = $this->tweaks;
154
+ return $variables;
155
+ }
156
+
157
+
158
+ }
readme.txt CHANGED
@@ -3,8 +3,8 @@ Contributors: whiteshadow
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A6P9S6CE3SRSW
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 4.1
6
- Tested up to: 4.9.4
7
- Stable tag: 1.8.2
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
@@ -63,6 +63,14 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
63
 
64
  == Changelog ==
65
 
 
 
 
 
 
 
 
 
66
  = 1.8.2 =
67
  * Fixed the PHP warning "count(): Parameter must be an array or an object that implements Countable in menu-editor-core.php".
68
  * Fixed a bug that could cause some network admin menus to be highlighted in green as if they were new.
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A6P9S6CE3SRSW
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 4.1
6
+ Tested up to: 4.9.5
7
+ Stable tag: 1.8.3
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
63
 
64
  == Changelog ==
65
 
66
+ = 1.8.3 =
67
+ * Added a couple of tutorial links to the settings page.
68
+ * Fixed a potential crash that was caused by a bug in the "WP Editor" plugin version 1.2.6.3.
69
+ * Fixed some obsolete callback syntax that was still using "&$this".
70
+ * Changed the order of some menu settings and added separators between groups of settings.
71
+ * Removed the "Screen Options" panel from AME tabs that didn't need it like "Plugins".
72
+ * Tested with WP 4.9.5.
73
+
74
  = 1.8.2 =
75
  * Fixed the PHP warning "count(): Parameter must be an array or an object that implements Countable in menu-editor-core.php".
76
  * Fixed a bug that could cause some network admin menus to be highlighted in green as if they were new.