Fieldmanager
  • Package
  • Function
  • Tree
  • Todo

Packages

  • Fieldmanager
    • Context
    • Datasource
    • Field
    • Util
  • None

Exceptions

  • FM_Class_Not_Found_Exception
  • FM_Developer_Exception
  • FM_Duplicate_Submenu_Name_Exception
  • FM_Exception
  • FM_Submenu_Not_Initialized_Exception
  • FM_Validation_Exception

Functions

  • _fieldmanager_registry
  • _fm_add_submenus
  • _fm_submenu_render
  • fieldmanager_enqueue_scripts
  • fieldmanager_get_baseurl
  • fieldmanager_get_template
  • fieldmanager_load_class
  • fieldmanager_load_file
  • fieldmanager_set_baseurl
  • fm_add_script
  • fm_add_style
  • fm_calculate_context
  • fm_get_context
  • fm_match_context
  • fm_register_submenu_page
  • fm_sanitize_textarea
  • fm_trigger_context_action
  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 
Fieldmanager API documentation generated by ApiGen 2.8.0