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