1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
|
<?php /** * Handles execution and rendering of HTML admin pages. */ class Loco_mvc_AdminRouter extends Loco_hooks_Hookable { /** * Current admin page controller * @var Loco_mvc_AdminController */ private $ctrl;
/** * admin_menu action callback */ public function on_admin_menu() {
// lowest capability required to see menu items is "loco_admin" // currently also the highest (and only) capability $cap = 'loco_admin'; $user = wp_get_current_user(); $super = is_super_admin( $user->ID ); // Ensure Loco permissions are set up for the first time, or nobody will have access at all if( ! get_role('translator') || ( $super && ! is_multisite() && ! $user->has_cap($cap) ) ){ Loco_data_Permissions::init(); $user->get_role_caps(); // <- rebuild }
// rendering hook for all menu items $render = [ $this, 'renderPage' ]; // main loco pages, hooking only if has permission if( $user->has_cap($cap) ){
$label = __('Loco Translate','loco-translate'); // translators: Page title for plugin home screen $title = __('Loco, Translation Management','loco-translate'); add_menu_page( $title, $label, $cap, 'loco', $render, 'dashicons-translation' ); // alternative label for first menu item which gets repeated from top level add_submenu_page( 'loco', $title, __('Home','loco-translate'), $cap, 'loco', $render );
$label = __('Themes','loco-translate'); // translators: Page title for theme translations $title = __('Theme translations ‹ Loco','loco-translate'); add_submenu_page( 'loco', $title, $label, $cap, 'loco-theme', $render );
$label = __('Plugins', 'loco-translate'); // translators: Page title for plugin translations $title = __('Plugin translations ‹ Loco','loco-translate'); add_submenu_page( 'loco', $title, $label, $cap, 'loco-plugin', $render );
$label = __('WordPress', 'loco-translate'); // translators: Page title for core WordPress translations $title = __('Core translations ‹ Loco', 'loco-translate'); add_submenu_page( 'loco', $title, $label, $cap, 'loco-core', $render );
$label = __('Languages', 'loco-translate'); // translators: Page title for installed languages page $title = __('Languages ‹ Loco', 'loco-translate'); add_submenu_page( 'loco', $title, $label, $cap, 'loco-lang', $render );
// settings page only for users with manage_options permission in addition to Loco access: if( $user->has_cap('manage_options') ){ $title = __('Plugin settings','loco-translate'); add_submenu_page( 'loco', $title, __('Settings','loco-translate'), 'manage_options', 'loco-config', $render ); } // but all users need access to user preferences which require standard Loco access permission else { $title = __('User options','loco-translate'); add_submenu_page( 'loco', $title, __('Settings','loco-translate'), $cap, 'loco-config-user', $render ); }
// string translation simulator if( loco_debugging() ){ $label = __('Debug', 'loco-translate'); add_submenu_page( 'loco', $label, $label, $cap, 'loco-debug', $render ); } } }
/** * Early hook as soon as we know what screen will be rendered * @return void */ public function on_current_screen( WP_Screen $screen ){ $action = isset($_GET['action']) ? $_GET['action'] : null; $this->initPage( $screen, $action ); }
/** * Instantiate admin page controller from current screen. * This is called early (before renderPage) so controller can listen on other hooks. * * @param string $action * @return Loco_mvc_AdminController|null */ public function initPage( WP_Screen $screen, $action = '' ){ $class = null; $args = []; // suppress error display when establishing Loco page $page = self::screenToPage($screen); if( is_string($page) ){ $class = self::pageToClass( $page, $action, $args ); } if( is_null($class) ){ $this->ctrl = null; return null; } // class should exist, so throw fatal if it doesn't $this->ctrl = new $class; if( ! $this->ctrl instanceof Loco_mvc_AdminController ){ throw new Exception( $class.' must inherit Loco_mvc_AdminController'); } // transfer flash messages from session to admin notice buffer try { $session = Loco_data_Session::get(); while( $message = $session->flash('success') ){ Loco_error_AdminNotices::success( $message ); } } catch( Exception $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); } // Initialise controller with query string + route arguments // note that $_GET is not being stripped of slashes added by WordPress. try { $this->ctrl->_init($_GET+$args); do_action('loco_admin_init', $this->ctrl ); } // catch errors during controller setup catch( Loco_error_Exception $e ){ $this->ctrl = new Loco_admin_ErrorController; // can't afford an error during an error try { $this->ctrl->_init( [ 'error' => $e ] ); } catch( Exception $_e ){ Loco_error_AdminNotices::debug( $_e->getMessage() ); Loco_error_AdminNotices::add($e); } } // WP emoji replacement doesn't inherit .wp-exclude-emoji so we'd have to add it to hundreds of elements. remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
return $this->ctrl; }
/** * Convert WordPress internal WPScreen $id into route prefix for an admin page controller * @return string|null */ private static function screenToPage( WP_Screen $screen ){ // Hooked menu slug is either "toplevel_page_loco" or "{title}_page_loco-{page}" // Sanitized {title} prefix is not reliable as it may be localized. instead just checking for "_page_loco" $id = $screen->id; $start = strpos($id,'_page_loco'); // not one of our pages if token not found if( is_int($start) ){ $page = substr( $id, $start+11 ) or $page = ''; return $page; } return null; }
/** * Get unvalidated controller class for given route parameters * Abstracted from initPage so we can validate routes in self::generate * @param string $page * @param string $action * @return string|null */ private static function pageToClass( $page, $action, array &$args ){ $routes = [ '' => 'Root', 'debug' => 'Debug', // site-wide plugin configurations 'config' => 'config_Settings', 'config-apis' => 'config_Apis', 'config-user' => 'config_Prefs', 'config-debug' => 'config_Debug', 'config-version' => 'config_Version', // bundle type listings 'theme' => 'list_Themes', 'plugin' => 'list_Plugins', 'core' => 'list_Core', 'lang' => 'list_Locales', // bundle level views '{type}-view' => 'bundle_View', '{type}-conf' => 'bundle_Conf', '{type}-setup' => 'bundle_Setup', 'lang-view' => 'bundle_Locale', // file initialization '{type}-msginit' => 'init_InitPo', '{type}-xgettext' => 'init_InitPot', '{type}-upload' => 'init_Upload', // file resource views '{type}-file-view' => 'file_View', '{type}-file-edit' => 'file_Edit', '{type}-file-info' => 'file_Info', '{type}-file-head' => 'file_Head', '{type}-file-diff' => 'file_Diff', '{type}-file-move' => 'file_Move', '{type}-file-delete' => 'file_Delete', // test routes that don't actually exist 'test-no-class' => 'test_NonExistentClass', ]; if( ! $page ){ $page = $action; } else if( $action ){ $page .= '-'. $action; } $args['_route'] = $page; // tokenize path arguments if( $page && preg_match('/^(plugin|theme|core)-/', $page, $r ) ){ $args['type'] = $r[1]; $page = substr_replace( $page, '{type}', 0, strlen($r[1]) ); } if( isset($routes[$page]) ){ return 'Loco_admin_'.$routes[$page].'Controller'; } // debug routing failures: // throw new Exception( sprintf('Failed to get page class from $page=%s',$page) ); return null; }
/** * Main entry point for admin menu callback, establishes page and hands off to controller * @return void */ public function renderPage(){ try { // show deferred failure from initPage if( ! $this->ctrl ){ throw new Loco_error_Exception( __('Page not found','loco-translate') ); } // display loco admin page echo $this->ctrl->render(); } catch( Exception $e ){ $ctrl = new Loco_admin_ErrorController; try { $ctrl->_init( [] ); } catch( Exception $_e ){ // avoid errors during error rendering Loco_error_AdminNotices::debug( $_e->getMessage() ); } echo $ctrl->renderError($e); } // ensure session always shutdown cleanly after render Loco_data_Session::close(); do_action('loco_admin_shutdown'); }
/** * Generate a routable link to Loco admin page * @param string $route * @return string */ public static function generate( $route, array $args = [] ){ $url = null; $page = null; $action = null; // empty action targets plugin root if( ! $route || 'loco' === $route ){ $page = 'loco'; } // support direct usage of page hooks else if( 'loco-' === substr($route,0,5) && menu_page_url($route,false) ){ $page = $route; } // else split action into admin page (e.g. "loco-themes") and sub-action (e.g. "view-theme") else { $page = 'loco'; $path = explode( '-', $route ); if( $sub = array_shift($path) ){ $page .= '-'.$sub; if( $path ){ $action = implode('-',$path); } } } // sanitize extended route in debug mode only. useful in tests if( loco_debugging() ){ $tmp = []; $class = self::pageToClass( (string) substr($page,5), $action, $tmp ); if( ! $class ){ throw new UnexpectedValueException( sprintf('Invalid admin route: %s', json_encode($route) ) ); } else { class_exists($class,true); // <- autoloader will throw if not class found } } // if url found, it should contain the page if( $url ){ unset( $args['page'] ); } // else start with base URL else { $url = admin_url('admin.php'); $args['page'] = $page; } // add action if found if( $action ){ $args['action'] = $action; } // else ensure not set in args, as it's reserved else { unset( $args['action'] ); } // append all arguments to base URL if( $query = http_build_query($args) ){ $sep = false === strpos($url, '?') ? '?' : '&'; $url .= $sep.$query; } return $url; }
}
|