PluginProbe
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI / 7.9.6
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI v7.9.6
8.0.5 8.0.4 8.0.3 8.0.1 8.0.2 8.0.0 7.9.9.7 7.9.9.6 7.9.9.5 7.9.9.4 7.9.9.3 7.9.9.2 7.9.9.1 7.9.9 7.9.8 7.9.7 7.9.6 7.9.5 7.9.4 7.9.3 7.9.2 7.9.1 7.9.0 7.8.9 7.8.8 All 47 releases
gamipress / libraries / cmb2-metatabs-options / code / cmb2_metatabs_options.php

cmb2_metatabs_options.php in GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI 7.9.6, at libraries/cmb2-metatabs-options/code/cmb2_metatabs_options.php

1,185 lines 37.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * See https://github.com/rogerlos/cmb2-metatabs-options
5 *
6 * GamiPress Team updates:
7 *
8 * Removed Javascript file checking by using the get_headers() function.
9 * Support to CMB2 2.7.0 by renaming CMB2_hookup instances to CMB2_Hookup.
10 * Support for PHP 5.6:
11 * - Moved auto-generated id into a object var.
12 * - Removed all usages of [] to instantiate arrays.
13 * - Moved all direct callbacks into a reference calls.
14 * On multisite installs, fixed undefined 'hook' index.
15 *
16 * General Notes
17 *
18 * @since 1.3 Adds reset options button, thanks @rubengc https://github.com/rubengc
19 * @since 1.1.2 Changed way empty string initially passed into filters
20 * @since 1.1.1 Now in "WordPress" code style
21 * @since 1.1.0 Discovered class did NOT handle multiple options pages; fixed by:
22 * - self::$props now keyed with random ID assigned on each __construct call
23 * - Many action callbacks now closures, to allow ID to be passed
24 * - Nearly every method now has ID as parameter
25 * @since 1.1.0 Fixed javascript loading and found and squished a couple of JS bugs
26 * @since 1.1.0 Made class a bit more flexible as to the types of pages it can display
27 * @since 1.1.0 Stamped out as many static methods as possible
28 *
29 * Important note when adding an option page via WP action: You MUST add this class after CMB2. The earliest you can
30 * add your page via action is:
31 *
32 * add_action( 'init', 'your_page_adding_callback_function', 9992 );
33 *
34 * CMB2 decremets the "priority" for each release, to avoid conflicts with various versions of their library;
35 * it stands at 9991 at the moment (CMB2 version 2.2.1). To avoid all possible conflicts, set the priority above 10000.
36 *
37 * You must add your page before admin_init, as that action is referenced within this class.
38 */
39 class Cmb2_Metatabs_Options {
40
41 /**
42 * Prevents settings notices from being repeated
43 *
44 * @var bool
45 *
46 * @since 1.0.0
47 */
48 protected static $once = FALSE;
49
50 /**
51 * self::$props: Properties needed to create options page(s)
52 * injected
53 * ['id'] array pay identification key N @since 1.1.0
54 * ['page'] string page slug N @since 1.1.0
55 * ['hook'] string WP page hook N @since 1.1.0
56 * ['key'] string WP Options slug Y @since 1.0.0
57 * ['title'] string Options page title Y @since 1.0.0
58 * ['topmenu'] string See ['menuargs']['parent_slug'] Y @since 1.0.0
59 * ['postslug'] string Option page to post menu Y @since 1.0.0
60 * ['jsuri'] string JS file for tab handling Y @since 1.0.0
61 * ['resettxt'] string reset text, empty removes button Y @since 1.0.0
62 * ['savetxt'] string Save text, empty removes button Y @since 1.0.0
63 * ['class'] string Class(es) added to wrapper Y @since 1.1.1
64 * ['cols'] int Columns; 1 or 2 Y @since 1.0.0
65 * ['regkey'] bool Register options key? Y @since 1.1.0
66 * ['getboxes'] bool False: no CMB2_Boxes::get_all Y @since 1.1.0
67 * ['plugincss'] bool False, no CSS, true, CSS Y @since 1.2
68 * ['admincss'] bool|string False, no CSS, string CSS to add Y @since 1.1.1
69 * ['menuargs'] array Used in WP add_[sub]menu_page Y @since 1.0.0
70 * ['parent_slug'] string Parent menu slug Y @since 1.0.2
71 * ['page_title'] string Page Title Y @since 1.0.2
72 * ['menu_title'] string Menu Title Y @since 1.0.2
73 * ['capability'] string WordPress capability Y @since 1.0.2
74 * ['menu_slug'] string Menu page slug Y @since 1.0.2
75 * ['icon_url'] string Top-level menu icon Y @since 1.0.2
76 * ['position'] int Top-level menu position Y @since 1.0.2
77 * ['network'] bool True, multisite network menu Y @since 1.1.0
78 * ['view_capability'] string WP capability to view page Y @since 1.2
79 * ['boxes'] array CMB2 metabox objects/IDs Y @since 1.0.0
80 * ['tabs'] array Array of tab config arrays Y @since 1.0.0
81 * [ array
82 * ['id'] string Tab ID
83 * ['title'] string Tab title
84 * ['desc'] string HTML shown above boxes on tab
85 * ['boxes'] array Plain array of CMB2 box ids
86 * ]
87 * ['load'] array Allows WP actions on page load Y @since 1.1.0
88 * [ array
89 * ['action'] string WP action; allows use of [hook]
90 * ['call'] callable Callable function
91 * ['priority'] int Priority (10 is default)
92 * ['args'] int Number of arguments (1 default)
93 * ]
94 *
95 * @var array
96 *
97 * @since 1.2 added 'plugincss' which if set to false removes plugin CSS, while still allowing 'admincss'
98 * @since 1.2 added 'view_capability': FALSE = do not check, '' = use 'capability', or WP capability string
99 * @since 1.1.1 added 'class', which allows extra class(es) to be added to wrapper div
100 * added 'admincss', false: no CSS; string extra CSS to add to page
101 * @since 1.1.0 moved injectible params (with default values) to self::$defaults
102 * - 'network' - can add page to multisite network menu
103 * - 'regkey' - optional not registering options key on construct
104 * - 'getboxes' - optional disabling CMB2_Boxes::get_all
105 * - 'boxes' array can contain CMB2 box IDs as well as objects
106 * - injection of page load actions (via 'load')
107 * - actually works with multiple options pages (!)
108 * @since 1.0.2 turned menuargs into array to match WP functions
109 * @since 1.0.0
110 */
111 private static $props = array();
112
113 /**
114 * Properties which can be injected via constructor, a subset of self::$props
115 *
116 * @var array $defaults ( See above )
117 *
118 * @since 1.2 added 'view_capability', 'plugincss'
119 * @since 1.1.1 added 'class'
120 * @since 1.1.0 moved from self::$props to prevent problems with multiple options pages
121 */
122 private $defaults = array(
123 'key' => 'my_options',
124 'regkey' => TRUE,
125 'title' => 'My Options',
126 'topmenu' => '',
127 'postslug' => '',
128 'class' => '',
129 'menuargs' => array(
130 'parent_slug' => '',
131 'page_title' => '',
132 'menu_title' => '',
133 'capability' => 'manage_options',
134 'menu_slug' => '',
135 'icon_url' => '',
136 'position' => NULL,
137 'network' => FALSE,
138 'view_capability' => '',
139 ),
140 'jsuri' => '',
141 'boxes' => array(),
142 'getboxes' => TRUE,
143 'plugincss' => TRUE,
144 'admincss' => '',
145 'tabs' => array(),
146 'cols' => 1,
147 'resettxt' => 'Reset',
148 'savetxt' => 'Save',
149 'load' => array(),
150 );
151
152 /**
153 * Auto-generated object ID
154 *
155 * @var string
156 *
157 * @since 1.3.1
158 */
159 protected $id;
160
161 /**
162 * Inject anything within the self::$defaults array by matching the argument keys.
163 *
164 * @param array $args Array of arguments, see self::$defaults
165 *
166 * @throws \Exception
167 *
168 * @since 1.1.0 only allowed within admin
169 * - uses $this->defaults instead of self::$props when parsing args
170 * - sets ID for self::$props
171 * - no longer always uses option key for page identifier
172 * - prevents double display if submenu page has same slug as parent
173 * @since 1.0.2 recurse the menuargs array with internal function instead of wp_parse_args()
174 * @since 1.0.0
175 */
176 public function __construct( $args ) {
177
178 // require CMB2
179 if ( ! class_exists( 'CMB2' ) ) {
180 throw new Exception( 'CMB2_Metatabs_Options: CMB2 is required to use this class.' );
181 }
182
183 // only allow within WP admin area;
184 if ( ! is_admin() ) {
185 return;
186 }
187
188 // set the ID
189 $this->id = $this->set_ID();
190
191 // parse any injected arguments and add to self::$props
192 self::$props[ $this->id ] = $this->parse_args_r( $args, $this->defaults );
193
194 // validate the properties we were sent
195 $this->validate_props( $this->id );
196
197 // if the menu_slug == parent_slug, set hide to true, prevents duplicate page display
198 self::$props[ $this->id ]['hide'] =
199 self::$props[ $this->id ]['menuargs']['parent_slug'] == self::$props[ $this->id ]['menuargs']['menu_slug'] &&
200 self::$props[ $this->id ]['menuargs']['parent_slug'] != '';
201
202 // add tabs: several actions depend on knowing if tabs are present
203 self::$props[ $this->id ]['tabs'] = $this->add_tabs();
204
205 // Add actions
206 $this->add_wp_actions();
207 }
208
209 /**
210 * Returns an ID unlikely to be already set
211 *
212 * @return string
213 *
214 * @since 1.1.0
215 */
216 private function set_ID() {
217
218 return 'cmo' . wp_rand( 1000, 9999 );
219 }
220
221 /**
222 * PARSE ARGUMENTS RECURSIVELY
223 * Allows us to merge multidimensional properties
224 *
225 * Thanks: https://gist.github.com/boonebgorges/5510970
226 *
227 * @param array $a
228 * @param array $b
229 *
230 * @return array
231 *
232 * @since 1.0.2
233 */
234 public function parse_args_r( &$a, $b ) {
235
236 $a = (array) $a;
237 $b = (array) $b;
238 $r = $b;
239 foreach ( $a as $k => &$v ) {
240 if ( is_array( $v ) && isset( $r[ $k ] ) ) {
241 $r[ $k ] = $this->parse_args_r( $v, $r[ $k ] );
242 } else {
243 $r[ $k ] = $v;
244 }
245 }
246
247 return $r;
248 }
249
250 /**
251 * Checks the values of critical passed properties
252 *
253 * @param string $this->id @since 1.1.0
254 *
255 * @throws \Exception
256 *
257 * @since 1.2 Added view_capability setting
258 * @since 1.1.0 Setting self::$props[$this->id][page]
259 * @since 1.0.2 Removed validation of menu args. Sending a plain array will no longer work!
260 * @since 1.0.1 Moved menuargs validation to within this method
261 * @since 1.0.0
262 */
263 private function validate_props() {
264
265 // if key or title do not exist, throw exception
266 if ( ! self::$props[ $this->id ]['key'] ) {
267 throw new Exception( 'CMB2_Metatabs_Options: Settings key missing.' );
268 }
269
270 // set JS url
271 if ( ! self::$props[ $this->id ]['jsuri'] ) {
272 self::$props[ $this->id ]['jsuri'] = plugin_dir_url( __FILE__ ) . 'cmb2multiopts.js';
273 }
274
275 // set columns to 1 if illegal value sent
276 self::$props[ $this->id ]['cols'] = intval( self::$props[ $this->id ]['cols'] );
277 if ( self::$props[ $this->id ]['cols'] > 2 || self::$props[ $this->id ]['cols'] < 1 ) {
278 self::$props[ $this->id ]['cols'] = 1;
279 }
280
281 // if menuargs[menu_slug] is set, change the page prop to that
282 self::$props[ $this->id ]['page'] = self::$props[ $this->id ]['menuargs']['menu_slug'] ?
283 self::$props[ $this->id ]['menuargs']['menu_slug'] : self::$props[ $this->id ]['key'];
284
285 // set page viewing capability; empty string = same as menuargs[capability], false = do not check
286 if ( ! self::$props[ $this->id ]['menuargs']['view_capability'] ) {
287 self::$props[ $this->id ]['menuargs']['view_capability'] =
288 self::$props[ $this->id ]['menuargs']['view_capability'] === '' ?
289 self::$props[ $this->id ]['menuargs']['capability'] : FALSE;
290 }
291 }
292
293 /**
294 * Some additional actions are added elsewhere as they cannot be added this early.
295 *
296 * @param string $this->id @since 1.1.0
297 *
298 * @since 1.1.0 menu can be added to multisite network menu
299 * - page load actions allowed
300 * - registering the options key optional
301 * @since 1.0.0
302 */
303 private function add_wp_actions() {
304
305 // Register setting
306 if ( self::$props[ $this->id ]['regkey'] ) {
307 add_action(
308 'admin_init',
309 array( $this, 'register' )
310 );
311 }
312
313 // Allow multisite network menu pages
314 $net = ( is_multisite() && self::$props[ $this->id ]['menuargs']['network'] === TRUE ) ? 'network_' : '';
315
316 // Adds page to admin
317 add_action(
318 $net . 'admin_menu',
319 array( $this, 'add_options_page' ),
320 12
321 );
322
323 // Include CSS for this options page as style tag in head, if tabs are configured
324 add_action(
325 'admin_head',
326 array( $this, 'add_css' )
327 );
328
329 // Adds JS to foot
330 add_action(
331 'admin_enqueue_scripts',
332 array( $this, 'add_scripts' )
333 );
334
335 // Adds custom save button field, allowing save button to be added to metaboxes
336 add_action(
337 'cmb2_render_options_save_button',
338 array( $this, 'render_options_save_button' ),
339 10, 1 );
340 }
341
342 /**
343 * Allows page to call WP actions on load
344 *
345 * @param string $this->id
346 *
347 * @since 1.1.0
348 */
349 private function load_actions() {
350
351 if ( empty( self::$props[ $this->id ]['load'] ) ) {
352 return;
353 }
354
355 foreach ( self::$props[ $this->id ]['load'] as $load ) {
356
357 // skip if no action or callback
358 if ( ! isset( $load['action'] ) || ! isset( $load['callback'] ) ) {
359 continue;
360 }
361
362 // skip if the [hook] token is not in the [action]
363 if ( strpos( $load['action'], '-[hook]' ) === FALSE ) {
364 continue;
365 }
366
367 // replace token with page hook
368 $load['action'] = str_replace( '[hook]', self::$props[ $this->id ]['hook'], $load['action'] );
369
370 // make sure if priority is int
371 $pri = isset( $load['priority'] ) && intval( $load['priority'] ) > 0 ? intval( $load['priority'] ) : 10;
372
373 // make sure args is int
374 $arg = isset( $load['args'] ) && intval( $load['args'] ) > 0 ? intval( $load['args'] ) : 1;
375
376 // add action
377 add_action( $load['action'], $load['callback'], $pri, $arg );
378 }
379 }
380
381 /**
382 * @param string $this->id @since 1.1.0
383 *
384 * @since 1.0.0
385 */
386 public function register() {
387
388 $options_key = sanitize_key( self::$props[ $this->id ]['key'] );
389
390 register_setting( $options_key, $options_key, array(
391 'type' => 'array',
392 'sanitize_callback' => 'cmb2_metatabs_options_settings_sanitization_cb',
393 ) );
394 }
395
396 /**
397 * @param string $this->id @since 1.1.0
398 *
399 * @since 1.1.0 Changed some action callbacks to closures
400 * - pages can now load actions
401 * @since 1.0.2 Moved the callback determination to build_menu_args()
402 * @since 1.0.0
403 */
404 public function add_options_page() {
405
406 // build arguments
407 $args = $this->build_menu_args( $this->id );
408
409 // this is kind of ugly, but so is the WP function!
410 self::$props[ $this->id ]['hook'] =
411 $args['cb']( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] );
412
413 // Include CMB CSS in the head to avoid FOUC, called here as we need the screen ID
414 add_action(
415 'admin_print_styles-' . self::$props[ $this->id ]['hook'],
416 array( 'CMB2_Hookup', 'enqueue_cmb_css' )
417 );
418
419 // Adds existing metaboxes, see note in function, called here as we need the screen ID
420 add_action(
421 'add_meta_boxes_' . self::$props[ $this->id ]['hook'],
422 array( $this, 'add_metaboxes' )
423 );
424
425 // On page load, do "metaboxes" actions, called here as we need the screen ID
426 add_action(
427 'load-' . self::$props[ $this->id ]['hook'],
428 array( $this, 'do_metaboxes' )
429 );
430
431 // Allows pages to call actions
432 $this->load_actions();
433 }
434
435 /**
436 * Builds the arguments needed to add options page to admin menu if they are not injected.
437 *
438 * Including either self::$props['topmenu'] or self::$props['menuargs']['parent_slug'] will trigger the creation
439 * of a submenu, otherwise a new top menu will be added.
440 *
441 * @param string $this->id @since 1.1.0
442 *
443 * @return array
444 *
445 * @since 1.0.2 Removed counting of menu arguments, function now uses menuargs keys
446 * @since 1.0.1 Removed menuargs validation to validate_props() method
447 * @since 1.0.0
448 */
449 private function build_menu_args() {
450
451 $args = array();
452
453 // set the top menu if either topmenu or the menuargs parent_slug is set
454 $parent = self::$props[ $this->id ]['topmenu'] ? self::$props[ $this->id ]['topmenu'] : '';
455 $parent = self::$props[ $this->id ]['menuargs']['parent_slug'] ?
456 self::$props[ $this->id ]['menuargs']['parent_slug'] : $parent;
457
458 // set the page title; overrides 'title' with menuargs 'page_title' if set
459 $pagetitle = self::$props[ $this->id ]['menuargs']['page_title'] ?
460 self::$props[ $this->id ]['menuargs']['page_title'] : self::$props[ $this->id ]['title'];
461
462 // sub[0] : parent slug
463 if ( $parent ) {
464 // add a post_type get variable, to allow post options pages, if set
465 $add = self::$props[ $this->id ]['postslug'] ? '?post_type=' . self::$props[ $this->id ]['postslug'] : '';
466 $args[] = $parent . $add;
467 }
468
469 // top[0], sub[1] : page title
470 $args[] = $pagetitle;
471
472 // top[1], sub[2] : menu title, defaults to page title if not set
473 $args[] = self::$props[ $this->id ]['menuargs']['menu_title'] ?
474 self::$props[ $this->id ]['menuargs']['menu_title'] : $pagetitle;
475
476 // top[2], sub[3] : capability
477 $args[] = self::$props[ $this->id ]['menuargs']['capability'];
478
479 // top[3], sub[4] : menu_slug, defaults to options slug if not set
480 $args[] = self::$props[ $this->id ]['menuargs']['menu_slug'] ?
481 self::$props[ $this->id ]['menuargs']['menu_slug'] : self::$props[ $this->id ]['key'];
482
483 // top[4], sub[5] : callable function
484 $args[] = array( $this, 'admin_page_display' );
485
486 // top menu icon and menu position
487 if ( ! $parent ) {
488
489 // top[5] icon url
490 $args[] = self::$props[ $this->id ]['menuargs']['icon_url'] ?
491 self::$props[ $this->id ]['menuargs']['icon_url'] : '';
492
493 // top[6] menu position
494 $args[] = self::$props[ $this->id ]['menuargs']['position'] === NULL ?
495 NULL : intval( self::$props[ $this->id ]['menuargs']['position'] );
496 } // sub[6] : unused, but returns consistent array
497 else {
498 $args[] = NULL;
499 }
500
501 // set which WP function will be called based on $parent
502 $args['cb'] = $parent ? 'add_submenu_page' : 'add_menu_page';
503
504 return $args;
505 }
506
507 /**
508 * Add WP's metabox script, either by itself or as dependency of the tabs script. Added only to this options page.
509 * If you roll your own script, note the localized values being passed here.
510 *
511 * @param string $this->id @since 1.1.0
512 *
513 * @throws \Exception
514 *
515 * @since 1.1.0 Added initial check for CMO page
516 * - Fixed bug with localizing page; was sending wrong key if menu_slug was configured
517 * @since 1.0.1 Always add postbox toggle, removed toggle from tab handler JS
518 * @since 1.0.0
519 */
520 public function add_scripts() {
521
522 global $hook_suffix;
523
524 if( ! isset( self::$props[ $this->id ]['hook'] ) ) {
525 return;
526 }
527
528 // do not run if not a CMO page
529 if ( $hook_suffix !== self::$props[ $this->id ]['hook'] ) {
530 return;
531 }
532
533 // 'postboxes' needed for metaboxes to work properly
534 wp_enqueue_script( 'postbox' );
535
536 // toggle the postboxes
537 add_action(
538 'admin_print_footer_scripts',
539 array( $this, 'toggle_postboxes' )
540 );
541
542 // only add the main script to the options page if there are tabs present
543 if ( empty( self::$props[ $this->id ]['tabs'] ) ) {
544 return;
545 }
546
547 // if self::$props['jsuri'] is empty, throw exception
548 if ( ! self::$props[ $this->id ]['jsuri'] ) {
549 throw new Exception( 'CMB2_Metatabs_Options: Tabs included but JS file not specified.' );
550 }
551
552 // enqueue the script
553 wp_enqueue_script(
554 self::$props[ $this->id ]['page'] . '-admin',
555 self::$props[ $this->id ]['jsuri'],
556 array( 'postbox' ),
557 '1.0.0',
558 TRUE
559 );
560
561 // localize script to give access to this page's slug
562 wp_localize_script( self::$props[ $this->id ]['page'] . '-admin', 'cmb2OptTabs', array(
563 'key' => self::$props[ $this->id ]['page'],
564 'posttype' => self::$props[ $this->id ]['postslug'],
565 'defaulttab' => self::$props[ $this->id ]['tabs'][0]['id'],
566 ) );
567 }
568
569 public function render_options_save_button() {
570 global $hook_suffix;
571
572 if ( $hook_suffix !== self::$props[ $this->id ]['hook'] || self::$props[ $this->id ]['hide'] ) {
573 return;
574 }
575
576 $args = func_get_args();
577 echo $this->render_save_button( $args[0]->args['desc'] );
578 }
579
580 /**
581 * Ensures boxes are toggleable on non tabs pages
582 *
583 * @since 1.0.0
584 */
585 public function toggle_postboxes() {
586
587 echo '<script>jQuery(document).ready(function(){postboxes.add_postbox_toggles("postbox-container");});</script>';
588 }
589
590 /**
591 * Adds a couple of rules to clean up WP styles if tabs are included
592 *
593 * @param string $this->id @since 1.1.0
594 *
595 * @since 1.2 checks 'plugincss' and does not add plugincss if set to false
596 * @since 1.1.1 added check of 'admincss' option to allow user to not add these styles
597 * same property allows user to add their own css to page
598 * added IDs to style tags
599 * @since 1.0.0
600 */
601 public function add_css() {
602
603 // if tabs are not being used, return
604 if ( empty( self::$props[ $this->id ]['tabs'] ) || self::$props[ $this->id ]['admincss'] === FALSE ) {
605 return;
606 }
607
608 $css = '';
609
610 // add css to clean up tab styles in admin when used in a postbox
611 if ( self::$props[ $this->id ]['plugincss'] === TRUE ) {
612 $css .= '<style type="text/css" id="CMO-cleanup-css">';
613 $css .= '.' . esc_html( self::$props[ $this->id ]['page'] ) . '.cmb2-options-page #poststuff h2.nav-tab-wrapper{padding-bottom:0;margin-bottom: 20px;}';
614 $css .= '.' . esc_html( self::$props[ $this->id ]['page'] ) . '.cmb2-options-page .opt-hidden{display:none;}';
615 $css .= '.' . esc_html( self::$props[ $this->id ]['page'] ) . '.cmb2-options-page #side-sortables{padding-top:22px;}';
616 $css .= '</style>';
617 }
618
619 // add user-injected CSS; added as separate style tag in case it is malformed
620 if ( ! empty( self::$props[ $this->id ]['admincss'] ) ) {
621 $css = '<style type="text/css" id="CMO-exta-css">';
622 $css .= esc_html( self::$props[ $this->id ]['admincss'] );
623 $css .= '</style>';
624 }
625
626 echo $css;
627 }
628
629 /**
630 * Adds CMB2 metaboxes.
631 *
632 * @since 1.1.0 ok to have no metaboxes (to allow text-only pages)
633 * - some action callbacks now closures
634 * @since 1.0.0
635 */
636 public function add_metaboxes() {
637
638 // get the metaboxes
639 self::$props[ $this->id ]['boxes'] = $this->cmb2_metaboxes( $this->id );
640
641 // exit this method if no metaboxes are present
642 if ( empty( self::$props[ $this->id ]['boxes'] ) ) {
643 return;
644 }
645
646 foreach ( self::$props[ $this->id ]['boxes'] as $box ) {
647
648 // skip if this should not be shown
649 if ( ! $this->should_show( $box, $this->id ) ) {
650 continue;
651 }
652
653 $mid = $box->meta_box['id'];
654
655 // add notice if settings are saved
656 add_action( 'cmb2_save_options-page_fields_' . $mid,
657 array( $this, 'settings_notices' ),
658 10, 2 );
659
660 // add callback if tabs are configured which hides metaboxes until moved into proper tabs if not in sidebar
661 if ( ! empty( self::$props[ $this->id ]['tabs'] ) && $box->meta_box['context'] !== 'side' ) {
662 add_filter( 'postbox_classes_' . self::$props[ $this->id ]['hook'] . '_' . $mid,
663 array( $this, 'hide_metabox_class' ) );
664 }
665
666 // if boxes are closed by default...
667 if ( $box->meta_box['closed'] ) {
668 add_filter( 'postbox_classes_' . self::$props[ $this->id ]['hook'] . '_' . $mid,
669 array( $this, 'close_metabox_class' ) );
670 }
671
672 // add meta box
673 add_meta_box(
674 $box->meta_box['id'],
675 $box->meta_box['title'],
676 array( $this, 'metabox_callback' ),
677 self::$props[ $this->id ]['hook'],
678 $box->meta_box['context'],
679 $box->meta_box['priority']
680 );
681 }
682 }
683
684 /**
685 * Mimics the CMB2 "should show" function to prevent boxes which should not be shown on this options page from
686 * appearing.
687 *
688 * @param CMB2 $box
689 *
690 * @return bool
691 *
692 * @since 1.1.0 Fixed bug with key vs page
693 * @since 1.0.0
694 */
695 private function should_show( $box ) {
696
697 // if the show_on key is not set, don't show
698 if ( ! isset( $box->meta_box['show_on']['key'] ) ) {
699 return FALSE;
700 }
701
702 // if the key is set but is not set to options-page, don't show
703 if ( $box->meta_box['show_on']['key'] != 'options-page' ) {
704 return FALSE;
705 }
706
707 // if this options key is not in the show_on value, don't show
708 if ( ! in_array( self::$props[ $this->id ]['page'], $box->meta_box['show_on']['value'] ) ) {
709 return FALSE;
710 }
711
712 return TRUE;
713 }
714
715 /**
716 * The "hidden" class hides metaboxes until they have been moved to appropriate tab, if tabs are used.
717 *
718 * @param array $classes
719 *
720 * @return array
721 *
722 * @since 1.0.0
723 */
724 public function hide_metabox_class( $classes ) {
725
726 $classes[] = 'opt-hidden';
727
728 return $classes;
729 }
730
731 /**
732 * Adds class to closed-by-default metaboxes
733 *
734 * @param array $classes
735 *
736 * @return array
737 *
738 * @since 1.0.0
739 */
740 public function close_metabox_class( $classes ) {
741
742 $classes[] = 'closed';
743
744 return $classes;
745 }
746
747 /**
748 * Triggers the loading of our metaboxes on this screen.
749 *
750 * @param string $this->id @since 1.1.0
751 *
752 * @since 1.0.0
753 */
754 public function do_metaboxes() {
755
756 do_action( 'add_meta_boxes_' . self::$props[ $this->id ]['hook'], NULL );
757 do_action( 'add_meta_boxes', self::$props[ $this->id ]['hook'], NULL );
758 }
759
760 /**
761 * Builds the fields and saves them.
762 *
763 * @param array $args @since 1.1.0
764 *
765 * @since 1.0.1 Refactored the save tests to method should_save()
766 * @since 1.0.0
767 */
768 public function metabox_callback( $post, $metabox ) {
769
770 // get the metabox, fishing the ID out of the arguments array
771 $cmb = cmb2_get_metabox( $metabox['id'], self::$props[ $this->id ]['key'] );
772
773 if ( $this->should_save( $cmb ) ) {
774 // save fields
775 $cmb->save_fields( self::$props[ $this->id ]['key'], $cmb->mb_object_type(), $_POST );
776 } else if ( $this->should_reset( $cmb ) ) {
777 // Reset fields
778 delete_option( self::$props[ $this->id ]['key'] );
779 }
780
781 // show the fields
782 $cmb->show_form();
783 }
784
785 /**
786 * Determine whether the CMB2 object should be reset. All tests must be true, hence return false for
787 * any failure.
788 *
789 * @param string $this->id @since 1.1.0
790 * @param \CMB2 $cmb
791 *
792 * @return bool
793 *
794 * @since 1.1.0 static unclung
795 * @since 1.0.3 made static method
796 * @since 1.0.1
797 */
798 private function should_reset( $cmb ) {
799
800 // are these values set?
801 if ( ! isset( $_POST['reset-cmb'], $_POST['object_id'], $_POST[ $cmb->nonce() ] ) ) {
802 return FALSE;
803 }
804
805 // does the nonce match?
806 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash ( $_POST[ $cmb->nonce() ] ) ), $cmb->nonce() ) ) {
807 return FALSE;
808 }
809
810 // does the object_id equal the settings key?
811 if ( ! $_POST['object_id'] == self::$props[ $this->id ]['key'] ) {
812 return FALSE;
813 }
814
815 return TRUE;
816 }
817
818 /**
819 * Determine whether the CMB2 object should be saved. All tests must be true, hence return false for
820 * any failure.
821 *
822 * @param \CMB2 $cmb
823 *
824 * @return bool
825 *
826 * @since 1.1.0 static unclung
827 * @since 1.0.3 made static method
828 * @since 1.0.1
829 */
830 private function should_save( $cmb ) {
831
832 // was this flagged to save fields?
833 if ( ! $cmb->prop( 'save_fields' ) ) {
834 return FALSE;
835 }
836
837 // are these values set?
838 if ( ! isset( $_POST['submit-cmb'], $_POST['object_id'], $_POST[ $cmb->nonce() ] ) ) {
839 return FALSE;
840 }
841
842 // does the nonce match?
843 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash ( $_POST[ $cmb->nonce() ] ) ), $cmb->nonce() ) ) {
844 return FALSE;
845 }
846
847 // does the object_id equal the settings key?
848 if ( ! $_POST['object_id'] == self::$props[ $this->id ]['key'] ) {
849 return FALSE;
850 }
851
852 return TRUE;
853 }
854
855 /**
856 * Admin page markup.
857 *
858 * @since 1.2 Checks user's capabilities to view page
859 * @since 1.1.0 Added page identifier to filters for easier multiple-page use
860 * - Moved page part compilers to separate functions for clarity
861 * @since 1.0.0
862 */
863 public function admin_page_display() {
864
865 // this is only set to true if a menu sub-item has the same slug as the parent
866 if ( self::$props[ $this->id ]['hide'] ) {
867 return;
868 }
869
870 // check page viewing capability
871 if ( self::$props[ $this->id ]['menuargs']['view_capability'] !== FALSE ) {
872 if ( ! current_user_can( self::$props[ $this->id ]['menuargs']['view_capability'] ) ) {
873 return;
874 }
875 }
876
877 // get top of page
878 $page = $this->admin_page_top( $this->id );
879
880 // if there are metaboxes to display, add form and boxes
881 if ( ! empty( self::$props[ $this->id ]['boxes'] ) ) {
882 $page .= $this->admin_page_form( $this->id );
883 }
884
885 // get bottom of page
886 $page .= $this->admin_page_bottom( $this->id );
887
888 echo $page;
889
890 // reset the notices flag
891 self::$once = FALSE;
892 }
893
894 /**
895 * Generates the top of the options page
896 *
897 * @return string
898 *
899 * @since 1.1.2 Instead of passing empty string directly in filter call, set it to var; allows cumulative filters
900 * @since 1.1.1 added ability to inject extra wrapper class(es)
901 * @since 1.1.0
902 */
903 private function admin_page_top() {
904
905 // standard classes, includes page id
906 $classes = 'wrap cmb2-options-page cmo-options-page ' . self::$props[ $this->id ]['page'];
907 $filterable = '';
908
909 // add any extra configured classes
910 if ( ! empty( self::$props[ $this->id ]['class'] ) ) {
911 $classes .= ' ' . self::$props[ $this->id ]['class'];
912 }
913
914 $ret = '<div class="' . $classes . '">';
915 $ret .= '<h2>' . esc_html( get_admin_page_title() ) . '</h2>';
916 $ret .= '<div class="cmo-before-form">';
917
918 // note this now passes the page slug as a second argument
919 $ret .= apply_filters( 'cmb2metatabs_before_form', $filterable, self::$props[ $this->id ]['page'] );
920
921 $ret .= '</div>';
922
923 return $ret;
924 }
925
926 /**
927 * Generate bottom of options page
928 *
929 * @param $this->id
930 *
931 * @return string
932 *
933 * @since 1.1.2 Instead of passing empty string directly in filter call, set it to var; allows cumulative filters
934 * @since 1.1.0
935 */
936 private function admin_page_bottom() {
937
938 $filterable = '';
939
940 $ret = '<div class="cmo-after-form">';
941
942 // note this now passes the page slug as a second argument
943 $ret .= apply_filters( 'cmb2metatabs_after_form', $filterable, self::$props[ $this->id ]['page'] );
944
945 $ret .= '</div>';
946 $ret .= '</div>';
947
948 return $ret;
949 }
950
951 /**
952 * Metaboxes and tabs display on options page
953 *
954 * @return string
955 *
956 * @since 1.2 Added WP nonces for box order and closed boxes
957 * @since 1.1.0 Moved admin page form to separate function to allow text-only options pages
958 */
959 private function admin_page_form() {
960
961 // form wraps all tabs
962 $ret = '<form class="cmb-form" method="post" id="cmo-options-form" '
963 . 'enctype="multipart/form-data" encoding="multipart/form-data">';
964
965 // hidden object_id field
966 $ret .= '<input type="hidden" name="object_id" value="' . self::$props[ $this->id ]['key'] . '">';
967
968 // wp nonce fields
969 $ret .= wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', FALSE, FALSE );
970 $ret .= wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', FALSE, FALSE );
971
972 // add postbox, which allows use of metaboxes
973 $ret .= '<div id="poststuff">';
974
975 // main column
976 $ret .= '<div id="post-body" class="metabox-holder columns-' . self::$props[ $this->id ]['cols'] . '">';
977
978 // if two columns are called for
979 if ( self::$props[ $this->id ]['cols'] == 2 ) {
980
981 // add markup for sidebar
982 $ret .= '<div id="postbox-container-1" class="postbox-container">';
983 $ret .= '<div id="side-sortables" class="meta-box-sortables ui-sortable">';
984
985 ob_start();
986
987 // add sidebar metaboxes
988 do_meta_boxes( self::$props[ $this->id ]['hook'], 'side', NULL );
989
990 $ret .= ob_get_clean();
991
992 $ret .= '</div></div>'; // close sidebar
993 }
994
995 // open postbox container
996 $ret .= '<div id="postbox-container-';
997 $ret .= self::$props[ $this->id ]['cols'] == 2 ? '2' : '1';
998 $ret .= '" class="postbox-container">';
999
1000 // add tabs; the sortables container is within each tab
1001 $ret .= $this->render_tabs( $this->id );
1002
1003 ob_start();
1004
1005 // place normal boxes, note that 'normal' and 'advanced' are rendered together when using tabs
1006 do_meta_boxes( self::$props[ $this->id ]['hook'], 'normal', NULL );
1007 do_meta_boxes( self::$props[ $this->id ]['hook'], 'advanced', NULL );
1008
1009 $ret .= ob_get_clean();
1010
1011 $ret .= '</div></div></div>';
1012
1013 // add submit button if resettxt or savetxt was included
1014 if ( self::$props[ $this->id ]['resettxt'] || self::$props[ $this->id ]['savetxt'] ) {
1015 $ret .= '<div style="clear:both;">';
1016 $ret .= $this->render_reset_button( self::$props[ $this->id ]['resettxt'] );
1017 $ret .= $this->render_save_button( self::$props[ $this->id ]['savetxt'] );
1018 $ret .= '</div>';
1019 }
1020
1021 $ret .= '</form>';
1022
1023 return $ret;
1024 }
1025
1026 /**
1027 * Renders reset button
1028 *
1029 * @param string $text
1030 *
1031 * @since 1.3
1032 * @return string
1033 */
1034 public function render_reset_button( $text = '' ) {
1035
1036 return $text ? '<input type="submit" name="reset-cmb" value="' . $text . '" class="button">' : '';
1037 }
1038
1039 /**
1040 * Renders save button
1041 *
1042 * @param string $text
1043 *
1044 * @since 1.1.0 CMB2 now invokes this from closure which already fished out the text string
1045 * @since 1.0.0
1046 * @return string
1047 */
1048 public function render_save_button( $text = '' ) {
1049
1050 return $text ? '<input type="submit" name="submit-cmb" value="' . esc_attr( $text ) . '" class="button-primary">' : '';
1051 }
1052
1053 /**
1054 * Added a check to make sure its only called once for the page...
1055 *
1056 * @since 1.0.1 updated text domain
1057 * @since 1.0.0
1058 */
1059 public function settings_notices( $object_id, $cmb_id ) {
1060
1061 // bail if this isn't a notice for this page or we've already added a notice
1062 if ( $object_id !== self::$props[ $this->id ]['key'] || empty( $cmb_id ) || self::$once ) {
1063 return;
1064 }
1065
1066 // add notifications
1067 add_settings_error( self::$props[ $this->id ]['key'] . '-notices', '', __( 'Settings updated.', 'cmb2' ), 'updated' );
1068 settings_errors( self::$props[ $this->id ]['key'] . '-notices' );
1069
1070 // set the flag so we don't pile up notices
1071 self::$once = TRUE;
1072 }
1073
1074 /**
1075 * Returns tabs, if they've been configured.
1076 *
1077 * @return string $return
1078 *
1079 * @since 1.0.0
1080 */
1081 private function render_tabs() {
1082
1083 if ( empty( self::$props[ $this->id ]['tabs'] ) ) {
1084 return '';
1085 }
1086
1087 $containers = '';
1088 $tabs = '';
1089
1090 foreach ( self::$props[ $this->id ]['tabs'] as $tab ) {
1091
1092 // add tabs navigation
1093 $tabs .= '<a href="#" id="opt-tab-' . esc_attr( $tab['id'] ) . '" class="nav-tab opt-tab" ';
1094 $tabs .= 'data-optcontent="#opt-content-' . esc_attr( $tab['id'] ) . '">';
1095 $tabs .= $tab['title'];
1096 $tabs .= '</a>';
1097
1098 // add tabs containers, javascript will use the data attribute to move metaboxes to within proper tab
1099 $contents = implode( ',', $tab['boxes'] );
1100
1101 // tab container markup
1102 $containers .= '<div class="opt-content" id="opt-content-' . esc_attr( $tab['id'] ) . '" ';
1103 $containers .= ' data-boxes="' . esc_attr( $contents ) . '">';
1104 $containers .= $tab['desc'];
1105 $containers .= '<div class="meta-box-sortables ui-sortable">';
1106 $containers .= '</div>';
1107 $containers .= '</div>';
1108 }
1109
1110 // add the tab structure to the page
1111 $return = '<h2 class="nav-tab-wrapper">';
1112 $return .= $tabs;
1113 $return .= '</h2>';
1114 $return .= $containers;
1115
1116 return $return;
1117 }
1118
1119 /**
1120 * Allows three methods of adding metaboxes:
1121 *
1122 * 1) Injected boxes are added to the boxes array
1123 * 2) Add additional boxes (or boxes if none were injected) the usual way within this function
1124 * 3) If array is still empty and getboxes === true, call CMB2_Boxes::get_all();
1125 *
1126 * @param string $this->id @since 1.1.0
1127 *
1128 * @return array|\CMB2[]
1129 *
1130 * @since 1.1.0 allow skipping of call to CMB2_Boxes::get_all();
1131 * - ok to pass CMB2 metabox ID or CMB2 object; previously only objects allowed
1132 * @since 1.0.0
1133 */
1134 private function cmb2_metaboxes() {
1135
1136 // add any injected metaboxes
1137 $boxes = self::$props[ $this->id ]['boxes'];
1138
1139 // if boxes is not empty, check to see if they're CMB2 objects, or strings
1140 if ( ! empty( $boxes ) ) {
1141 foreach ( $boxes as $key => $box ) {
1142 if ( ! is_object( $box ) ) {
1143 $boxes[ $key ] = CMB2_Boxes::get( $box );
1144 }
1145 }
1146 }
1147
1148 // if $boxes is still empty and getboxes is true, try grabbing boxes from CMB2
1149 $boxes = ( empty( $boxes ) && self::$props[ $this->id ]['getboxes'] === TRUE ) ? CMB2_Boxes::get_all() : $boxes;
1150
1151 return $boxes;
1152 }
1153
1154 /**
1155 * Add tabs to your options page. The array is empty by default. You can inject them into the constructor,
1156 * or add them here, or leave empty for no tabs.
1157 *
1158 * @param string $this->id @since 1.1.0
1159 *
1160 * @return array
1161 *
1162 * @since 1.0.0
1163 */
1164 private function add_tabs() {
1165
1166 $tabs = self::$props[ $this->id ]['tabs'];
1167
1168 return $tabs;
1169 }
1170 }
1171
1172 if( ! function_exists( 'cmb2_metatabs_options_settings_sanitization_cb' ) ) {
1173 function cmb2_metatabs_options_settings_sanitization_cb( $settings ) {
1174 global $cmb2_metatabs_options_settings_sanitizated;
1175
1176 if( $cmb2_metatabs_options_settings_sanitizated === true ) {
1177 return $settings;
1178 }
1179
1180 $cmb2_metatabs_options_settings_sanitizated = true;
1181
1182 return map_deep( $settings, 'sanitize_text_field' );
1183 }
1184
1185 }