1 <?php
2 /**
3 * Fieldmanager Base Plugin File.
4 *
5 * @package Fieldmanager
6 * @version 1.0.0-beta.3
7 */
8
9 /*
10 Plugin Name: Fieldmanager
11 Plugin URI: https://github.com/alleyinteractive/wordpress-fieldmanager
12 Description: Add fields to content types programatically.
13 Author: Austin Smith
14 Version: 1.0.0-beta.3
15 Author URI: http://www.alleyinteractive.com/
16 */
17
18 /**
19 * Current version of Fieldmanager.
20 */
21 define( 'FM_VERSION', '1.0.0-beta.3' );
22
23 /**
24 * Filesystem path to Fieldmanager.
25 */
26 define( 'FM_BASE_DIR', dirname( __FILE__ ) );
27
28 /**
29 * Default version number for static assets registered via Fieldmanager.
30 */
31 define( 'FM_GLOBAL_ASSET_VERSION', 1 );
32
33 /**
34 * Whether to display debugging information. Default is value of WP_DEBUG.
35 */
36 if ( !defined( 'FM_DEBUG' ) ) {
37 define( 'FM_DEBUG', WP_DEBUG );
38 }
39
40 /**
41 * Load a Fieldmanager class based on a class name.
42 *
43 * Understands Fieldmanager nomenclature to find the right file within the
44 * plugin, but does not know how to load classes outside of that. If you build
45 * your own field, you will have to include it or autoload it for yourself.
46 *
47 * @see fieldmanager_load_file() for more detail about possible return values.
48 *
49 * @param string $class Class name to load.
50 * @return mixed The result of fieldmanager_load_file() or void if the class is
51 * not found.
52 */
53 function fieldmanager_load_class( $class ) {
54 if ( class_exists( $class ) || 0 !== strpos( $class, 'Fieldmanager' ) ) {
55 return;
56 }
57 $class_id = strtolower( substr( $class, strrpos( $class, '_' ) + 1 ) );
58
59 if ( 0 === strpos( $class, 'Fieldmanager_Context' ) ) {
60 if ( 'context' == $class_id ) {
61 return fieldmanager_load_file( 'context/class-fieldmanager-context.php' );
62 }
63 return fieldmanager_load_file( 'context/class-fieldmanager-context-' . $class_id . '.php' );
64 }
65
66 if ( 0 === strpos( $class, 'Fieldmanager_Datasource' ) ) {
67 if ( 'datasource' == $class_id ) {
68 return fieldmanager_load_file( 'datasource/class-fieldmanager-datasource.php' );
69 }
70 return fieldmanager_load_file( 'datasource/class-fieldmanager-datasource-' . $class_id . '.php' );
71 }
72 return fieldmanager_load_file( 'class-fieldmanager-' . $class_id . '.php', $class );
73 }
74
75
76 if ( function_exists( 'spl_autoload_register' ) ) {
77 spl_autoload_register( 'fieldmanager_load_class' );
78 }
79
80 /**
81 * Load a Fieldmanager file.
82 *
83 * @throws FM_Class_Not_Found_Exception.
84 *
85 * @param string $file File to load.
86 */
87 function fieldmanager_load_file( $file ) {
88 $file = FM_BASE_DIR . '/php/' . $file;
89 if ( !file_exists( $file ) ) {
90 throw new FM_Class_Not_Found_Exception( $file );
91 }
92 require_once( $file );
93 }
94
95 // Load utility classes with helper functions.
96 fieldmanager_load_file( 'util/class-fieldmanager-util-term-meta.php' );
97 fieldmanager_load_file( 'util/class-fieldmanager-util-validation.php' );
98
99 /**
100 * Enqueue CSS and JS in the Dashboard.
101 */
102 function fieldmanager_enqueue_scripts() {
103 wp_enqueue_script( 'fieldmanager_script', fieldmanager_get_baseurl() . 'js/fieldmanager.js', array( 'jquery' ), '1.0.7' );
104 wp_enqueue_style( 'fieldmanager_style', fieldmanager_get_baseurl() . 'css/fieldmanager.css', array(), '1.0.4' );
105 wp_enqueue_script( 'jquery-ui-sortable' );
106 }
107 add_action( 'admin_enqueue_scripts', 'fieldmanager_enqueue_scripts' );
108
109 /**
110 * Tell Fieldmanager that it has a base URL somewhere other than the plugins URL.
111 *
112 * @param string $path The URL to Fieldmanager, excluding "fieldmanager/", but
113 * including a trailing slash.
114 */
115 function fieldmanager_set_baseurl( $path ) {
116 _fieldmanager_registry( 'baseurl', trailingslashit( $path ) );
117 }
118
119 /**
120 * Get the Fieldmanager base URL.
121 *
122 * @return string The URL pointing to the top Fieldmanager.
123 */
124 function fieldmanager_get_baseurl() {
125 $path_override = _fieldmanager_registry( 'baseurl' );
126 if ( $path_override ) {
127 return $path_override;
128 }
129 return plugin_dir_url( __FILE__ );
130 }
131
132 /**
133 * Get the path to a field template.
134 *
135 * @param string $tpl_slug The name of a template file inside the "templates/"
136 * directory, excluding ".php".
137 * @return string The template path, or the path to "textfield.php" if the
138 * requested template is not found.
139 */
140 function fieldmanager_get_template( $tpl_slug ) {
141 if ( ! file_exists( plugin_dir_path( __FILE__ ) . 'templates/' . $tpl_slug . '.php' ) ) {
142 $tpl_slug = 'textfield';
143 }
144 return plugin_dir_path( __FILE__ ) . 'templates/' . $tpl_slug . '.php';
145 }
146
147 /**
148 * Enqueue a script with a closure, optionally localizing data to it.
149 *
150 * @see wp_enqueue_script() for detail about $handle, $deps, $ver, and $in_footer.
151 * @see wp_localize_script() for detail about $data_object and $data.
152 * @see FM_GLOBAL_ASSET_VERSION for detail about the fallback value of $ver.
153 * @see fieldmanager_get_baseurl() for detail about the fallback value of $plugin_dir.
154 *
155 * @param string $handle Script name.
156 * @param string $path The path to the file inside $plugin_dir.
157 * @param array $deps Script dependencies. Default empty array.
158 * @param string|bool $ver Script version. Default none.
159 * @param bool $in_footer Whether to render the script in the footer. Default false.
160 * @param string $data_object The $object_name in wp_localize_script(). Default none.
161 * @param array $data The $l10n in wp_localize_script(). Default empty array.
162 * @param string $plugin_dir The base URL to the directory with the script. Default none.
163 * @param bool $admin Unused.
164 */
165 function fm_add_script( $handle, $path, $deps = array(), $ver = false, $in_footer = false, $data_object = '', $data = array(), $plugin_dir = '', $admin = true ) {
166 if ( !is_admin() ) {
167 return;
168 }
169 if ( !$ver ) {
170 $ver = FM_GLOBAL_ASSET_VERSION;
171 }
172 if ( '' == $plugin_dir ) {
173 $plugin_dir = fieldmanager_get_baseurl(); // allow overrides for child plugins
174 }
175 $add_script = function() use ( $handle, $path, $deps, $ver, $in_footer, $data_object, $data, $plugin_dir ) {
176 wp_enqueue_script( $handle, $plugin_dir . $path, $deps, $ver, $in_footer );
177 if ( !empty( $data_object ) && !empty( $data ) ) {
178 wp_localize_script( $handle, $data_object, $data );
179 }
180 };
181
182 add_action( 'admin_enqueue_scripts', $add_script );
183 add_action( 'wp_enqueue_scripts', $add_script );
184 }
185
186 /**
187 * Register and enqueue a style with a closure.
188 *
189 * @see wp_enqueue_script() for detail about $handle, $path, $deps, $ver, and $media.
190 * @see FM_GLOBAL_ASSET_VERSION for detail about the fallback value of $ver.
191 * @see fieldmanager_get_baseurl() for detail about base URL.
192 *
193 * @param string $handle Stylesheet name.
194 * @param string $path Path to the file inside of the Fieldmanager base URL.
195 * @param array $deps Stylesheet dependencies. Default empty array.
196 * @param string|bool Stylesheet version. Default none.
197 * @param string $media Media for this stylesheet. Default 'all'.
198 * @param bool $admin Unused.
199 */
200 function fm_add_style( $handle, $path, $deps = array(), $ver = false, $media = 'all', $admin = true ) {
201 if( !is_admin() ) {
202 return;
203 }
204 if ( !$ver ) {
205 $ver = FM_GLOBAL_ASSET_VERSION;
206 }
207 $add_script = function() use ( $handle, $path, $deps, $ver, $media ) {
208 wp_register_style( $handle, fieldmanager_get_baseurl() . $path, $deps, $ver, $media );
209 wp_enqueue_style( $handle );
210 };
211
212 add_action( 'admin_enqueue_scripts', $add_script );
213 add_action( 'wp_enqueue_scripts', $add_script );
214 }
215
216 /**
217 * Get or set values from a simple, static registry.
218 *
219 * Keeps the globals out.
220 *
221 * @param string $var The variable name to set.
222 * @param mixed $val The value to store for $var. Default null.
223 * @return mixed The stored value of $var if $val is null, or false if $val is
224 * null and $var was not set in the registry, or void if $val is being set.
225 */
226 function _fieldmanager_registry( $var, $val = null ) {
227 static $registry;
228 if ( !is_array( $registry ) ) {
229 $registry = array();
230 }
231 if ( null === $val ) {
232 return isset( $registry[ $var ] ) ? $registry[ $var ] : false;
233 }
234 $registry[ $var ] = $val;
235 }
236
237 /**
238 * Get the context for triggers and pattern matching.
239 *
240 * This function is crucial for performance. It prevents the unnecessary
241 * initialization of FM classes, and the unnecessary loading of CSS and
242 * JavaScript.
243 *
244 * @see fm_calculate_context() for detail about the returned array values.
245 *
246 * @param bool $recalculate Optional. If true, FM will recalculate the current
247 * context. This is necessary for testing and perhaps
248 * other programmatic purposes.
249 * @return array Contextual information for the current request.
250 */
251 function fm_get_context( $recalculate = false ) {
252 static $calculated_context;
253
254 if ( ! $recalculate && $calculated_context ) {
255 return $calculated_context;
256 } else {
257 $calculated_context = fm_calculate_context();
258 return $calculated_context;
259 }
260 }
261
262 /**
263 * Calculate contextual information for the current request.
264 *
265 * You can't use this function to determine whether or not a context "form" will
266 * be displayed, since it can be used anywhere. We would love to use
267 * get_current_screen(), but it's not available in some POST actions, and
268 * generally not available early enough in the init process.
269 *
270 * This is a function to watch closely as WordPress changes, since it relies on
271 * paths and variables.
272 *
273 * @return array {
274 * Array of context information.
275 *
276 * @type string|null A Fieldmanager context of "post", "quickedit", "term",
277 * "submenu", or "user", or null if one isn't found.
278 * @type string|null A "type" dependent on the context. For "post" and
279 * "quickedit", the post type. For "term", the taxonomy.
280 * For "submenu", the group name. For all others, null.
281 * }
282 */
283 function fm_calculate_context() {
284 // Safe to use at any point in the load process, and better than URL matching.
285 if ( is_admin() ) {
286 $script = substr( $_SERVER['PHP_SELF'], strrpos( $_SERVER['PHP_SELF'], '/' ) + 1 );
287
288 /*
289 * Calculate a submenu context.
290 *
291 * For submenus of the default WordPress menus, the submenu's parent
292 * slug should match the requested script. For submenus of custom menu
293 * pages, where "admin.php" is the requested script but not the parent
294 * slug, the submenu's slug should match the GET request.
295 *
296 * @see fm_register_submenu_page() for detail about $submenu array values.
297 */
298 if ( ! empty( $_GET['page'] ) ) {
299 $page = sanitize_text_field( $_GET['page'] );
300 $submenus = _fieldmanager_registry( 'submenus' );
301
302 if ( isset( $_GET['post_type'] ) ) {
303 $post_type = sanitize_text_field( $_GET['post_type'] );
304 if ( post_type_exists( $post_type ) ) {
305 $script .= "?post_type={$post_type}";
306 }
307 }
308
309 if ( $submenus ) {
310 foreach ( $submenus as $submenu ) {
311 if ( $script == $submenu[0] || ( 'admin.php' == $script && $page == $submenu[4] ) ) {
312 return array( 'submenu', $page );
313 }
314 }
315 }
316 }
317
318 switch ( $script ) {
319 // Context = "post".
320 case 'post.php':
321 if ( !empty( $_POST['action'] ) && ( 'editpost' === $_POST['action'] || 'newpost' === $_POST['action'] ) ) {
322 $calculated_context = array( 'post', sanitize_text_field( $_POST['post_type'] ) );
323 } elseif ( !empty( $_GET['post'] ) ) {
324 $calculated_context = array( 'post', get_post_type( intval( $_GET['post'] ) ) );
325 }
326 break;
327 case 'post-new.php':
328 $calculated_context = array( 'post', !empty( $_GET['post_type'] ) ? sanitize_text_field( $_GET['post_type'] ) : 'post' );
329 break;
330 // Context = "user".
331 case 'profile.php':
332 case 'user-edit.php':
333 $calculated_context = array( 'user', null );
334 break;
335 // Context = "quickedit".
336 case 'edit.php':
337 $calculated_context = array( 'quickedit', !empty( $_GET['post_type'] ) ? sanitize_text_field( $_GET['post_type'] ) : 'post' );
338 break;
339 case 'admin-ajax.php':
340 // Passed in via an Ajax form.
341 if ( !empty( $_POST['fm_context'] ) ) {
342 $subcontext = !empty( $_POST['fm_subcontext'] ) ? sanitize_text_field( $_POST['fm_subcontext'] ) : null;
343 $calculated_context = array( sanitize_text_field( $_POST['fm_context'] ), $subcontext );
344 } elseif ( !empty( $_POST['screen'] ) && !empty( $_POST['action'] ) ) {
345 if ( 'edit-post' === $_POST['screen'] && 'inline-save' === $_POST['action'] ) {
346 $calculated_context = array( 'quickedit', sanitize_text_field( $_POST['post_type'] ) );
347 // Context = "term".
348 } elseif ( 'add-tag' === $_POST['action'] && !empty( $_POST['taxonomy'] ) ) {
349 $calculated_context = array( 'term', sanitize_text_field( $_POST['taxonomy'] ) );
350 }
351 // Context = "quickedit".
352 } elseif ( !empty( $_GET['action'] ) && 'fm_quickedit_render' === $_GET['action'] ) {
353 $calculated_context = array( 'quickedit', sanitize_text_field( $_GET['post_type'] ) );
354 }
355 break;
356 // Context = "term".
357 case 'edit-tags.php':
358 case 'term.php': // As of 4.5-alpha; see https://core.trac.wordpress.org/changeset/36308
359 if ( !empty( $_POST['taxonomy'] ) ) {
360 $calculated_context = array( 'term', sanitize_text_field( $_POST['taxonomy'] ) );
361 } elseif ( !empty( $_GET['taxonomy'] ) ) {
362 $calculated_context = array( 'term', sanitize_text_field( $_GET['taxonomy'] ) );
363 }
364 break;
365 }
366 }
367
368 if ( empty( $calculated_context ) ) {
369 $calculated_context = array( null, null );
370 }
371
372 return $calculated_context;
373 }
374
375 /**
376 * Check whether a context is active.
377 *
378 * @see fm_get_context() for detail about the array values this function tries
379 * to match.
380 *
381 * @param string $context The Fieldmanager context to check for.
382 * @param string|array $type Type or types to check for. Default null.
383 * @return bool True if $context is "form". If $type is null, true if $context
384 * matches the first value of fm_get_context(). If $type is a string or
385 * array, true if the second value of fm_get_context() matches the string or
386 * is in the array and the first value matches $context. False otherwise.
387 */
388 function fm_match_context( $context, $type = null ) {
389 if ( 'form' == $context ) {
390 // Nothing to check, since forms can be anywhere.
391 return true;
392 }
393
394 $calculated_context = fm_get_context();
395 if ( $calculated_context[0] == $context ) {
396 if ( null !== $type ) {
397 if ( is_array( $type ) ) {
398 return in_array( $calculated_context[1], $type );
399 }
400 return ( $calculated_context[1] == $type );
401 }
402 return true;
403 }
404 return false;
405 }
406
407 /**
408 * Fire an action for the current Fieldmanager context, if it exists.
409 *
410 * @see fm_calculate_context() for detail about the values that determine the
411 * name of the action. Two actions are defined, but only one at most fires.
412 */
413 function fm_trigger_context_action() {
414 $calculated_context = fm_get_context();
415 if ( empty( $calculated_context[0] ) ) {
416 return;
417 }
418
419 list( $context, $type ) = $calculated_context;
420
421 if ( $type ) {
422 /**
423 * Fires when a specific Fieldmanager context and type load.
424 *
425 * The dynamic portions of the hook name, $context and $type, refer to
426 * the values returned by fm_calculate_context(). For example, the Edit
427 * screen for the Page post type would fire "fm_post_page".
428 *
429 * @param string $type The context subtype, e.g. the post type, taxonomy
430 * name, submenu option name.
431 */
432 do_action( "fm_{$context}_{$type}", $type );
433 }
434
435 /**
436 * Fires when any Fieldmanager context loads.
437 *
438 * The dynamic portion of the hook name, $context, refers to the first
439 * value returned by fm_calculate_context(). For example, the Edit User
440 * screen would fire "fm_user".
441 *
442 * @param string|null $type The context subtype, e.g. the post type,
443 * taxonomy name, submenu option name. null if this
444 * context does not have a subtype.
445 */
446 do_action( "fm_{$context}", $type );
447 }
448 add_action( 'init', 'fm_trigger_context_action', 99 );
449
450 /**
451 * Add data about a submenu page to the Fieldmanager registry under a slug.
452 *
453 * @see Fieldmanager_Context_Submenu for detail about $parent_slug, $page_title,
454 * $menu_title, $capability, and $menu_slug.
455 *
456 * @throws FM_Duplicate_Submenu_Name_Exception.
457 *
458 * @param string $group_name A slug to register the submenu page under.
459 * @param string $parent_slug Parent menu slug name or admin page file name.
460 * @param string $page_title Page title.
461 * @param string $menu_title Menu title. Falls back to $page_title if not set. Default null.
462 * @param string $capability Capability required to access the page. Default "manage_options".
463 * @param string $menu_slug Unique slug name for this submenu. Falls back to
464 * $group_name if not set. Default null.
465 */
466 function fm_register_submenu_page( $group_name, $parent_slug, $page_title, $menu_title = null, $capability = 'manage_options', $menu_slug = null ) {
467 $submenus = _fieldmanager_registry( 'submenus' );
468 if ( !$submenus ) {
469 $submenus = array();
470 }
471 if ( isset( $submenus[ $group_name ] ) ) {
472 throw new FM_Duplicate_Submenu_Name_Exception( sprintf( esc_html__( '%s is already in use as a submenu name', 'fieldmanager' ), $group_name ) );
473 }
474
475 if ( !$menu_title ) {
476 $menu_title = $page_title;
477 }
478
479 /**
480 * These data will be used to add a Fieldmanager_Context_Submenu instance to
481 * the Fieldmanager registry if this submenu page is active.
482 *
483 * @see Fieldmanager_Field::activate_submenu_page().
484 */
485 $submenus[ $group_name ] = array( $parent_slug, $page_title, $menu_title, $capability, $menu_slug ?: $group_name, '_fm_submenu_render' );
486
487 _fieldmanager_registry( 'submenus', $submenus );
488 }
489
490 /**
491 * Render a submenu page registered through the Fieldmanager registry.
492 *
493 * @see _fm_add_submenus().
494 *
495 * @throws FM_Submenu_Not_Initialized_Exception.
496 */
497 function _fm_submenu_render() {
498 $context = _fieldmanager_registry( 'active_submenu' );
499 if ( !is_object( $context ) ) {
500 throw new FM_Submenu_Not_Initialized_Exception( esc_html__( 'The Fieldmanger context for this submenu was not initialized', 'fieldmanager' ) );
501 }
502 $context->render_submenu_page();
503 }
504
505 /**
506 * Register submenu pages from the Fieldmanager registry.
507 */
508 function _fm_add_submenus() {
509 $submenus = _fieldmanager_registry( 'submenus' );
510 if ( !is_array( $submenus ) ) {
511 return;
512 }
513 foreach ( $submenus as $s ) {
514 call_user_func_array( 'add_submenu_page', $s );
515 }
516 }
517 add_action( 'admin_menu', '_fm_add_submenus', 15 );
518
519 /**
520 * Sanitize multi-line text.
521 *
522 * @param string $value Unsanitized text.
523 * @return string Text with each line of $value passed through sanitize_text_field().
524 */
525 function fm_sanitize_textarea( $value ) {
526 return implode( "\n", array_map( 'sanitize_text_field', explode( "\n", $value ) ) );
527 }
528
529 /**
530 * Stripslashes_deep for submenu data.
531 */
532 add_filter( 'fm_submenu_presave_data', 'stripslashes_deep' );
533
534 /**
535 * Exception class for Fieldmanager's fatal errors.
536 *
537 * Used mostly to differentiate in unit tests.
538 *
539 * @package Fieldmanager
540 */
541 class FM_Exception extends Exception { }
542
543 /**
544 * Exception class for classes that could not be loaded.
545 *
546 * @package Fieldmanager
547 */
548 class FM_Class_Not_Found_Exception extends Exception { }
549
550 /**
551 * Exception class for unitialized submenus.
552 *
553 * @package Fieldmanager
554 */
555 class FM_Submenu_Not_Initialized_Exception extends Exception { }
556
557 /**
558 * Exception class for duplicate submenu names.
559 *
560 * @package Fieldmanager
561 */
562 class FM_Duplicate_Submenu_Name_Exception extends Exception { }
563
564 /**
565 * Exception class for Fieldmanager's developer errors.
566 *
567 * Used mostly to differentiate in unit tests. This exception is meant to help
568 * developers write correct Fieldmanager implementations.
569 *
570 * @package Fieldmanager
571 */
572 class FM_Developer_Exception extends Exception { }
573
574 /**
575 * Exception class for Fieldmanager's validation errors.
576 *
577 * Used mostly to differentiate in unit tests. Validation errors in WordPress
578 * are not really recoverable.
579 *
580 * @package Fieldmanager
581 */
582 class FM_Validation_Exception extends Exception { }
583