Headline
CVE-2023-5252: fareharbor.php in fareharbor/tags/3.6.7 – WordPress Plugin Repository
The FareHarbor plugin for WordPress is vulnerable to Stored Cross-Site Scripting via shortcodes in versions up to, and including, 3.6.7 due to insufficient input sanitization and output escaping on user supplied attributes. This makes it possible for authenticated attackers with contributor-level and above permissions to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
1<?php2 /*3 Plugin Name: FareHarbor for WordPress4 Plugin URI: https://fareharbor.com/help/setup/wordpress-plugin/5 Description: Easily add FareHarbor reservation calendars and buttons to your site6 Version: 3.6.77 Author: FareHarbor8 Author URI: https://fareharbor.com9 */1011 // Make sure this file isn’t loaded on its own12 // -----------------------------------------------1314 defined( ‘ABSPATH’ ) or die( ‘Lost? <a href="/">Return home.</a>’ );1516 // Add init hook17 // -----------------------------------------------1819 add_action( 'init’, array( 'fareharbor’, ‘init’ ) );202122 // Global functions for potential backwards compatability23 // -----------------------------------------------2425 function fh_shortcode( $attrs ) {26 return fareharbor::fareharbor_shortcode( $attrs );27 }2829 function lightframe_shortcode( $attrs, $content = ‘’ ) {30 return fareharbor::lightframe_shortcode( $attrs, $content );31 }3233 function partners_shortcode( $attrs ) {34 return fareharbor::partners_shortcode( $attrs );35 }363738 // The important stuff39 // -----------------------------------------------4041 final class fareharbor {4243 // This class is just a namespace for static methods & variables44 private function __construct() {}4546 public static $version = '3.6.7’;4748 // Update the saved version number in the wp_options table49 // ===============================================5051 public static function init() {5253 // In a multisite environment some themes may wish to disable54 // this plugin.55 if ( apply_filters( 'fareharbor/disabled’, false ) )56 return;5758 // Register the shortcodes59 // -----------------------------------------------6061 add_shortcode( 'fareharbor’, array( 'fareharbor’, ‘fareharbor_shortcode’ ) );62 add_shortcode( 'lightframe’, array( 'fareharbor’, ‘lightframe_shortcode’ ) );63 add_shortcode( 'partners’, array( 'fareharbor’, ‘partners_shortcode’ ) );64 add_shortcode( 'itemgrid’, array( 'fareharbor’, ‘itemgrid_shortcode’ ) );65 // add_shortcode( 'bookembed’, array( 'fareharbor’, ‘bookembed_shortcode’ ) );6667 // Register our Settings Page & Settings68 // -----------------------------------------------6970 add_action( 'admin_menu’, array( 'fareharbor’, ‘register_options_page’ ) );71 add_action( 'admin_init’, array( 'fareharbor’, ‘register_settings’ ) );727374 // Include the script in the footer75 // -----------------------------------------------7677 add_action( 'wp_footer’, array( 'fareharbor’, ‘lightframe_api_footer’ ) );787980 // Maybe include fh-kit styles in header81 // -----------------------------------------------82 // Depends on option being set in the admin dashboard83 add_action( 'wp_enqueue_scripts’, array( 'fareharbor’, ‘maybe_enqueue_fh_kit_styles’ ) );8485 $saved_version = get_option( ‘fareharbor_version’ );86 if ( $saved_version !== self::$version )87 self::upgrade( $saved_version );8889 }9091 // Upgrade92 // ===============================================9394 private static function upgrade( $from ) {9596 if ( !$from ) {97 // New install or upgrade from version before 3.0, before we saved98 // version numbers in the database. No choice but to treat those99 // as a new install.100 // No settings should be saved but let’s be careful not to override101 // just in case102 $saved_settings = get_option( 'fareharbor_settings’, array() );103 if ( !is_array( $saved_settings ) )104 // Even less likely, but again just in case105 $saved_settings = array();106107 $defaults = array(108 ‘fh_responsive_calendars’ => 'on’,109 ‘fh_default_fallback’ => 'simple’,110 ‘fh_auto_lightframe_enabled’ => 'on’,111 );112 update_option( 'fareharbor_settings’, $saved_settings + $defaults );113114 // All new installs from v3.4 on should use v2 of the stylesheet115 update_option( 'fareharbor_kit_version’, ‘v2’ );116 }117 elseif ( version_compare( $from, '3.4’, ‘<’ ) ) {118 // Can’t swap version of old sites that might already be using v1119 // of the stylesheet!120 update_option( 'fareharbor_kit_version’, ‘v1’ );121 }122123 update_option( 'fareharbor_version’, self::$version, true );124125 }126127 // Script source128 // ===============================================129130 public static function lightframe_api_footer() {131132 $src = ‘https://’ . self::url() . '/embeds/api/v1/’;133134 if ( self::get_option( ‘fh_auto_lightframe_enabled’ ) )135 $src .= '?autolightframe=yes’;136137 echo "<!-- FareHarbor plugin activated --><script src=\"$src\"></script>";138139 }140141142 // FH-Kit Styles143 // ===============================================144145 public static function maybe_enqueue_fh_kit_styles() {146147 if ( self::is_lite() )148 return;149150 if( !self::get_option( ‘fh_buttons_active’ ) )151 return;152153 $query_string = self::get_option( ‘fh_buttons_query’ );154155 $query_string = strip_tags( $query_string );156 $query_string = trim( $query_string );157 $query_string = ltrim( $query_string, ‘?’ );158159 // Default to v1 for sites that don’t have this in the options table in160 // case upgrade procedure didn’t run. All new installs should get161 // `fareharbor_kit_version` saved as v2 upon install.162 $version = get_option( 'fareharbor_kit_version’, ‘v1’ );163 // Make the version filterable164 $version = apply_filters( 'fareharbor/fh_kit_version’, $version );165 // Validate. If we got something funky (as opposed to nothing at all) then166 // we’ll default to version 2.167 if ( !in_array( $version, array( 'v1’, 'v2’, ), true ) )168 $version = 'v2’;169170 $fh_kit_src = "https://fh-kit.com/buttons/$version/";171172 if ( $query_string )173 $fh_kit_src .= ‘?’ . $query_string;174175 $fh_kit_src = apply_filters( 'fareharbor/buttons_style_src’, $fh_kit_src );176177 // the 4th parameter is $version. Leaving it out causes WP to178 // add ver={current-wp-version} to the url. passing null stops this.179 wp_enqueue_style( 'fh-buttons’, $fh_kit_src, array(), null );180181 }182183184 // Default Arguments185 // ===============================================186187 private static $shared_defaults,188 $lightframe_defaults,189 $calendar_defaults,190 $partners_defaults,191 $itemgrid_defaults,192 $bookembed_defaults;193194 public static function _clear_options() {195 // This method exists for testing only.196 self::$options = null;197 self::$shared_defaults = null;198 self::$lightframe_defaults = null;199 self::$calendar_defaults = null;200 self::$partners_defaults = null;201 self::$itemgrid_defaults = null;202 self::$bookembed_defaults = null;203 }204205 private static function init_defaults() {206207 if ( isset( self::$shared_defaults ) )208 return;209210 self::$shared_defaults = array(211212 ‘shortname’ => self::get_option( 'fh_default_shortname’, ‘’ ),213 ‘items’ => '’,214 ‘asn’ => self::get_option( 'fh_default_asn’, ‘’ ),215 ‘asn_ref’ => self::get_option( 'fh_default_asn_ref’, ‘’ ),216 ‘ref’ => self::get_option( 'fh_default_ref’, ‘’ ),217 ‘lightframe’ => self::get_option( 'fh_default_lightframe’, ‘yes’ ),218 ‘full_items’ => self::get_option( 'fh_default_full_items’, ‘no’ ),219 ‘sheet’ => self::get_option( 'fh_default_sheet’, ‘’ ),220 ‘sheet_uuid’ => '’,221 ‘schedule’ => self::get_option( 'fh_default_schedule’, ‘’ ),222 ‘schedule_uuid’ => '’,223 ‘fallback’ => 'simple’,224 ‘flow’ => '’,225 ‘language’ => self::get_option( 'fh_default_language’, ‘’ ),226 ‘branding’ => '’,227 ‘bookable_only’ => '’,228229 );230231 self::$bookembed_defaults =232 self::$lightframe_defaults = array(233234 // ‘class’ => '’,235 // ‘style’ => '’,236 // ‘id’ => '’,237 ‘view’ => 'items’,238 ‘view_item’ => '’,239 ‘view_availability’ => '’,240241 );242 self::$bookembed_defaults[ ‘fallback’ ] = '’;243244 self::$calendar_defaults = array();245246 if ( self::get_option( 'fh_responsive_calendars’, false ) )247 self::$calendar_defaults[ ‘type’ ] = 'calendar’;248 else249 self::$calendar_defaults[ ‘type’ ] = 'calendar-small’;250251 self::$partners_defaults = array(252253 ‘include’ => '’,254255 );256257 self::$itemgrid_defaults = array();258259 }260261262 // [lightframe][/lightframe] shortcode263 // ===============================================264265 public static function lightframe_shortcode( $attrs, $content = ‘’ ) {266267 $link_attrs = self::lightframe_link_attrs( $attrs );268269 if ( $error = self::maybe_handle_shortcode_error( $link_attrs ) )270 return $error;271272 if ( !empty( $attrs[ ‘class’ ] ) )273 $link_attrs[ ‘class’ ] = $attrs[ ‘class’ ];274 elseif ( $default_class = self::get_option( ‘fh_default_class’ ) )275 $link_attrs[ ‘class’ ] = $default_class;276277 if ( !empty( $attrs[ ‘id’ ] ) )278 $link_attrs[ ‘id’ ] = $attrs[ ‘id’ ];279280 if ( !empty( $attrs[ ‘style’ ] ) )281 $link_attrs[ ‘style’ ] = $attrs[ ‘style’ ];282283 $link_attrs = array_map( 'strip_tags’, $link_attrs );284285 $link_attrs_string = '’;286 foreach ( $link_attrs as $name => $value )287 $link_attrs_string .= " $name=\"$value\"";288289 if ( trim( $content ) )290 $content = do_shortcode( $content );291292 return "<a$link_attrs_string>$content</a>";293294 }295296 public static function lightframe_link_attrs( $args ) {297298 if ( !isset( self::$lightframe_defaults ) )299 self::init_defaults();300301 $args = self::process_args( $args, self::$lightframe_defaults, ‘lightframe’ );302303 if ( isset( $args[ ‘error’ ] ) )304 return $args;305306 if ( $args[ ‘view_availability’ ] && !$args[ ‘view_item’ ] )307 return array( ‘error’ => ‘view_availability but no view_item’ );308309 $fallback_url = self::lightframe_fallback_url( $args );310311 $api_options = self::lightframe_api_options( $args );312 $json = strtr( json_encode( $api_options ), '"’, “’” );313314 return array(315 ‘href’ => $fallback_url,316 ‘target’ => '_blank’,317 ‘onclick’ => "FH.open($json); return false;",318 );319320 }321322 private static function lightframe_fallback_url( $args, $is_bookembed = false ) {323324 // The bookembed url structure is very similar to the simple fallback url structure325 $is_simple_fallback = ( $args[ ‘fallback’ ] === ‘’ ) || ( $args[ ‘fallback’ ] === ‘simple’ ) || $is_bookembed;326327 $url = ‘https://’ . self::url() . '/’;328329 if ( $is_bookembed )330 $url .= 'embeds/script/book/’;331 elseif ( $is_simple_fallback )332 $url .= 'embeds/book/’;333334 $url .= $args[ ‘shortname’ ] . '/’;335336 if ( !$is_simple_fallback337 || $args[ ‘view’ ] === 'all-availability’338 || $args[ ‘view_item’ ]339 ) {340 $url .= 'items/’;341 }342343 if ( $args[ ‘view_item’ ] ) {344345 $url .= $args[ ‘view_item’ ] . '/’;346347 if ( $args[ ‘view_availability’ ] )348 $url .= ‘availability/’ . $args[ ‘view_availability’ ] . '/book/’;349 elseif ( $args[ ‘full_items’ ] === ‘no’ )350 $url .= 'calendar/’;351352 } elseif ( $args[ ‘view’ ] === ‘all-availability’ ) {353354 $url .= 'calendar/’;355356 }357358 $query = self::get_shared_args_array( $args, 'dash’, $is_simple_fallback, false, true );359360 if ( $is_simple_fallback && $args[ ‘items’ ] )361 $query[ ‘selected-items’ ] = $args[ ‘items’ ];362363 if ( $query )364 $url .= ‘?’ . http_build_query( $query );365366 return $url;367368 }369370 private static function lightframe_api_options( $args ) {371372 $lo = array( ‘shortname’ => $args[ ‘shortname’ ] );373374 // Filter the visible items375 if ( $args[ ‘items’ ] )376 // putting it in an array so it gets brackets in json_encode377 $lo[ ‘items’ ] = explode( ',’, $args[ ‘items’ ] );378379 // Set the view380 if ( $args[ ‘view_item’ ] ) {381382 $lo[ ‘view’ ] = array( ‘item’ => $args[ ‘view_item’ ] );383384 if ( $args[ ‘view_availability’ ] )385 $lo[ ‘view’ ][ ‘availability’ ] = $args[ ‘view_availability’ ];386387 } elseif ( in_array( $args[ ‘view’ ], array( 'items’, ‘all-availability’ ), true ) ) {388389 $lo[ ‘view’ ] = $args[ ‘view’ ];390391 } else {392393 $lo[ ‘view’ ] = 'items’;394395 }396397 // Modifiers: shared args like fallback, asn, asn_ref, etc.398 $lo += self::get_shared_args_array( $args, 'camel’, true, true, true );399400 return $lo;401402 }403404405 // [bookembed] shortcode406 // ===============================================407408 public static function bookembed_shortcode( $attrs ) {409410 $script_src = self::bookembed_script_src( $attrs );411412 $maybe_error = self::maybe_handle_shortcode_error( $script_src );413 return $maybe_error ? $maybe_error : "<script src=\"$script_src\"></script>";414415 }416417 public static function bookembed_script_src( $args ) {418419 if ( !isset( self::$bookembed_defaults ) )420 self::init_defaults();421422 $args = self::process_args( $args, self::$bookembed_defaults, ‘bookembed’ );423424 if ( isset( $args[ ‘error’ ] ) )425 return $args;426427 if ( $args[ ‘view_availability’ ] && !$args[ ‘view_item’ ] )428 return array( ‘error’ => ‘view_availability but no view_item’ );429430 return self::lightframe_fallback_url( $args, true );431432 }433434435 // [fareharbor] shortcode (calendar)436 // ===============================================437438 public static function fareharbor_shortcode( $attrs ) {439440 $script_src = self::calendar_script_src( $attrs );441442 $maybe_error = self::maybe_handle_shortcode_error( $script_src );443 return $maybe_error ? $maybe_error : "<script src=\"$script_src\"></script>";444445 }446447 public static function calendar_script_src( $args ) {448449 if ( !isset( self::$calendar_defaults ) )450 self::init_defaults();451452 $args = self::process_args( $args, self::$calendar_defaults, ‘fareharbor’ );453454 if ( isset( $args[ ‘error’ ] ) )455 return $args;456457 $type = $args[ ‘type’ ];458 if ( $type === ‘small’ )459 $type = 'calendar-small’;460 elseif ( $type === ‘large’ || $type === ‘responsive’ )461 $type = 'calendar’;462463 $url_suffix = '’;464 if ( $args[ ‘items’ ] )465 $url_suffix = ‘items/’ . $args[ ‘items’ ] . '/’;466467 return self::embed_script_src( $type, $args, $url_suffix, array(), false, true );468469 }470471472 // [partners] shortcode473 // ===============================================474475 public static function partners_shortcode( $attrs ) {476477 $script_src = self::partners_script_src( $attrs );478479 $maybe_error = self::maybe_handle_shortcode_error( $script_src );480 return $maybe_error ? $maybe_error : “<script src=\"$script_src\"></script>";481482 }483484 public static function partners_script_src( $args ) {485486 if ( !isset( self::$partners_defaults ) )487 self::init_defaults();488489 $args = self::process_args( $args, self::$partners_defaults, ‘partners’ );490491 if ( isset( $args[ ‘error’ ] ) )492 return $args;493494 $query = array();495 if ( $args[ ‘include’ ] )496 $query[ ‘include’ ] = $args[ ‘include’ ];497498 return self::embed_script_src( 'partners’, $args, '’, $query, true, false );499500 }501502503 // [items] shortcode504 // ===============================================505506 public static function itemgrid_shortcode( $attrs ) {507508 $script_src = self::itemgrid_script_src( $attrs );509510 $maybe_error = self::maybe_handle_shortcode_error( $script_src );511 return $maybe_error ? $maybe_error : “<script src=\"$script_src\"></script>";512513 }514515516 public static function itemgrid_script_src( $args ) {517518 if ( !isset( self::$itemgrid_defaults ) )519 self::init_defaults();520521 $args = self::process_args( $args, self::$itemgrid_defaults, ‘itemgrid’ );522523 if ( isset( $args[ ‘error’ ] ) )524 return $args;525526 $query = array();527 if ( $args[ ‘items’ ] )528 $query[ ‘selected-items’ ] = $args[ ‘items’ ];529530 return self::embed_script_src( 'items’, $args, '’, $query, true, true );531532 }533534535 // Shared Shortcode Functionality536 // ===============================================537538 // Handling Argument Input539 // -----------------------------------------------540541 private static function process_args( $args, $defaults, $shortcode_name ) {542543 if ( !isset( self::$shared_defaults ) )544 self::init_defaults();545546 $defaults += self::$shared_defaults;547548 if ( !is_array( $args ) ) // Just in case.549 $args = $defaults;550551 // Trim whitespace && Strip Tags552 $args = array_map( array( __CLASS__, ‘sanitize’ ), $args );553554 // Strip smart quotes, because WordPress returns them as part of the value if the shortcode was set up using them555 $args = str_replace(556 array( “\xe2\x80\x98", “\xe2\x80\x99", “\xe2\x80\x9c", “\xe2\x80\x9d", chr(145), chr(146), chr(147), chr(148) ),557 array( '’, '’, '’, '’, '’, '’, '’, ‘’ ),558 $args559 );560561 // Merge in defaults562 $args = shortcode_atts( $defaults, $args, $shortcode_name );563564 // Confirm there’s a shortname. All of our shortcodes require this565 if ( !$args[ ‘shortname’ ] )566 return array( ‘error’ => ‘no shortname’ );567568 // Shortnames only work if lowercase569 $args[ ‘shortname’ ] = strtolower( $args[ ‘shortname’ ] );570571 // Clean up item IDs and included companies because users can’t be trusted572 $csv_keys = array( 'items’, 'view_item’, ‘include’ );573 foreach ( $csv_keys as $key )574 if ( isset( $args[ $key ] ) )575 $args[ $key ] = self::sanitize_csv( $args[ $key ] );576577 if ( $shortcode_name === ‘lightframe’ || $shortcode_name === ‘bookembed’ ) {578579 if ( !$args[ ‘view_item’ ] ) {580 // See if we’re filtering to just one item581 // in that case we treat it as if it were582 // passed to view_item instead583 $items_array = explode( ',’, $args[ ‘items’ ] );584 if ( count( $items_array ) === 1 ) {585 $args[ ‘view_item’ ] = $items_array[0];586 $args[ ‘items’ ] = '’;587 }588 }589590 }591592 return $args;593594 }595596 public static function sanitize($value)597 {598 $sanitized_value = preg_replace('/[^a-zA-Z0-9-_\s,:\/\.=&]/’, '’, $value);599600 return trim(strip_tags($sanitized_value));601 602 }603604 private static function sanitize_csv( $value ) {605606 return rtrim( str_replace( ' ', '’, $value ), ‘,’ );607608 }609610611 // Handling Errors612 // -----------------------------------------------613614 private static $error_messages = array(615616 'no shortname’617 => '<p>Please provide a FareHarbor shortname. (Format: <code>shortname="yourshortname"</code>)</p>’,618 'view_availability but no view_item’619 => '<p>Please provide <code>view_item</code> if using <code>view_availability</code>.</p>’,620621 );622623 private static function maybe_handle_shortcode_error( $output ) {624625 if ( is_array( $output ) && !empty( $output[ ‘error’ ] ) ) {626627 return isset( self::$error_messages[ $output[ ‘error’ ] ] )628 ? self::$error_messages[ $output[ ‘error’ ] ]629 : ‘<p>’ . $output[ ‘error’ ] . '</p>’;630631 }632633 return false;634635 }636637638 // Scripts for embeds (in use by [partners] and [fareharbor])639 // -----------------------------------------------640641 private static function embed_script_src( $type, $args, $url_suffix, $query, $include_full_items, $include_flow ) {642643 $script_src = ‘https://’ . self::url() . '/embeds/script/’644 . $type645 . '/’646 . $args[ ‘shortname’ ]647 . '/’648 . $url_suffix;649650 $args[ ‘lightframe’ ] = strtolower( trim( $args[ ‘lightframe’ ] ) );651 if ( $args[ ‘lightframe’ ] === ‘no’ )652 $query += array( ‘lightframe’ => $args[ ‘lightframe’ ] );653654 $query += self::get_shared_args_array( $args, 'dash’, $include_full_items, true, $include_flow );655656 $query_string = http_build_query( $query );657658 return $script_src . ( $query_string ? “?$query_string” : ‘’ );659660 }661662663 // Shared arguments664 // -----------------------------------------------665666 private static function get_shared_args_array( $args, $casing, $include_full_items, $include_fallback, $include_flow ) {667668 $out = array();669670 if ( $include_fallback && in_array( $args[ ‘fallback’ ], array( 'simple’, ‘classic’ ) ) )671 $out[ ‘fallback’ ] = $args[ ‘fallback’ ];672673 if ( $include_full_items && $args[ ‘full_items’ ] !== ‘no’ )674 $out[ ‘full_items’ ] = $args[ ‘full_items’ ];675676 if ( $args[ ‘asn’ ] ) {677678 $out[ ‘asn’ ] = $args[ ‘asn’ ];679680 if ( $args[ ‘asn_ref’ ] )681 $out[ ‘asn_ref’ ] = $args[ ‘asn_ref’ ];682683 }684685 if ( $args[ ‘ref’ ] )686 $out[ ‘ref’ ] = $args[ ‘ref’ ];687688 if ( $args[ ‘sheet’ ] )689 $out[ ‘sheet’ ] = $args[ ‘sheet’ ];690691 if ( $args[ ‘sheet_uuid’ ] )692 $out[ ‘sheet_uuid’ ] = $args[ ‘sheet_uuid’ ];693694 if ( $args[ ‘schedule’ ] )695 $out[ ‘schedule’ ] = $args[ ‘schedule’ ];696697 if ( $args[ ‘schedule_uuid’ ] )698 $out[ ‘schedule_uuid’ ] = $args[ ‘schedule_uuid’ ];699700 if ( $args[ ‘language’ ] )701 $out[ ‘language’ ] = $args[ ‘language’ ];702 703 if ( $args[ ‘branding’ ])704 $out[ ‘branding’ ] = $args[ ‘branding’ ];705706 if ( $args[ ‘bookable_only’ ])707 $out[ ‘bookable_only’ ] = $args[ ‘bookable_only’ ];708709710 if ( $include_flow && $args[ ‘flow’ ] )711 $out[ ‘flow’ ] = $args[ ‘flow’ ];712713 return self::fix_array_key_casing( $out, $casing );714715 }716717 private static function fix_array_key_casing( $in, $casing ) {718719 $out = array();720 foreach ( $in as $key => $value )721 $out[ self::fix_casing( $key, $casing ) ] = $value;722723 return $out;724725 }726727 private static function fix_casing( $text, $casing ) {728 // Assumes $text is already in snake_case729 switch ( $casing ) {730731 case 'camel’:732733 // ucwords() doesn’t support a $delimeters param734 // until php 5.4.32/5.5.16735 $text = strtr( $text, '_’, ' ' );736 $text = ucwords( $text );737 $text = str_replace( ' ', '’, $text );738 // lcfirst() doesn’t exist until php 5.3.0739 $text[0] = strtolower( $text[0] );740 return $text;741742 case 'dash’:743744 return strtr( $text, '_’, '-' );745746 }747748 return $text;749750 }751752753 // FareHarbor URL with optional FH_ENVIRONMENT754 // -----------------------------------------------755756 private static function url() {757758 $environment = ( defined( ‘FH_ENVIRONMENT’ ) && FH_ENVIRONMENT ) ? FH_ENVIRONMENT : '’;759 $environment = apply_filters( 'fareharbor/environment’, $environment );760761 return trim( $environment )762 ? trim( $environment ) . '.fareharbor.com’763 : 'fareharbor.com’;764765 }766767768 // Settings & Settings Pages769 // ===============================================770771 // Settings772 // -----------------------------------------------773774 private static function is_lite() {775 // This filter allows fh-kit and the admin page776 // to be turned off with:777 // add_filter( 'fareharbor/lite’, ‘__return_true’ );778 return apply_filters( 'fareharbor/lite’, false );779780 }781782 private static $options;783784 private static function get_option( $option_name, $default = '’, $filtered = true ) {785786 $filter_name = str_replace( 'fh_’, '’, $option_name );787 $filter_name = str_replace( 'default_’, 'defaults/’, $filter_name );788 $filter_name = str_replace( 'buttons_’, 'buttons/’, $filter_name );789 $filter_name = “fareharbor/$filter_name";790791 if ( self::is_lite() )792 return $filtered ? apply_filters( $filter_name, $default ) : $default;793794 if ( !isset( self::$options ) )795 self::$options = get_option( ‘fareharbor_settings’ );796797 $value = isset( self::$options[ $option_name ] )798 ? self::$options[ $option_name ]799 : $default;800801 if ( $filtered )802 $value = apply_filters( $filter_name, $value );803804 return self::sanitize($value);805806 }807808809 // The settings page810 // -----------------------------------------------811812 public static function register_options_page() {813814 if ( self::is_lite() )815 return;816817 add_options_page(818 'FareHarbor Plugin Settings’,819 'FareHarbor’,820 'manage_options’,821 __FILE__,822 array( __CLASS__, ‘render_options_page’ )823 );824825 }826827 public static function register_settings() {828829 if ( self::is_lite() )830 return;831832 register_setting(833 'fareharbor_settings’,834 'fareharbor_settings’,835 array( __CLASS__, ‘validate_settings’ )836 );837838 add_settings_section(839 'fh_about_section’,840 'About’,841 array( __CLASS__, ‘render_fh_about_section_text’ ),842 __FILE__843 );844845 add_settings_section(846 'fh_shortcode_defaults_section’,847 'Shortcode Defaults’,848 array( __CLASS__, ‘render_fh_shortcode_defaults_section_text’ ),849 __FILE__850 );851852 add_settings_field(853 'fh_default_shortname’,854 'shortname’,855 array( __CLASS__, ‘render_input_field’ ),856 __FILE__,857 'fh_shortcode_defaults_section’,858 array(859 ‘option_name’ => 'fh_default_shortname’,860 ‘description’ => 'Your company\’s FareHarbor shortname.’,861 )862 );863864 add_settings_field(865 'fh_responsive_calendars’,866 'Responsive Calendars’,867 array( __CLASS__, ‘render_input_field’ ),868 __FILE__,869 'fh_shortcode_defaults_section’,870 array(871 ‘option_name’ => 'fh_responsive_calendars’,872 ‘type’ => 'checkbox’,873 ‘label’ => 'Use responsive calendars by default.’,874 ‘description’ => 'This is the same as [fareharbor type="responsive”].’,875 )876 );877878 add_settings_field(879 'fh_default_asn’,880 'asn’,881 array( __CLASS__, ‘render_input_field’ ),882 __FILE__,883 'fh_shortcode_defaults_section’,884 array(885 ‘option_name’ => 'fh_default_asn’,886 ‘description’ => 'The shortname of your partner company. Include if using the ASN network.’,887 )888 );889890 add_settings_field(891 'fh_default_asn_ref’,892 'asn_ref’,893 array( __CLASS__, ‘render_input_field’ ),894 __FILE__,895 'fh_shortcode_defaults_section’,896 array(897 ‘option_name’ => 'fh_default_asn_ref’,898 ‘description’ => 'The voucher number that should be set for ASN bookings.’,899 )900 );901902 add_settings_field(903 'fh_default_ref’,904 'ref’,905 array( __CLASS__, ‘render_input_field’ ),906 __FILE__,907 'fh_shortcode_defaults_section’,908 array(909 ‘option_name’ => 'fh_default_ref’,910 ‘description’ => 'The online booking reference that should be set for bookings’,911 )912 );913914 add_settings_field(915 'fh_default_sheet’,916 'sheet’,917 array( __CLASS__, ‘render_input_field’ ),918 __FILE__,919 'fh_shortcode_defaults_section’,920 array(921 ‘option_name’ => 'fh_default_sheet’,922 ‘description’ => 'The price sheet that should be used while creating bookings.’,923 )924 );925926 add_settings_field(927 'fh_default_schedule’,928 'schedule’,929 array( __CLASS__, ‘render_input_field’ ),930 __FILE__,931 'fh_shortcode_defaults_section’,932 array(933 ‘option_name’ => 'fh_default_schedule’,934 ‘description’ => 'The price schedule that should be used while creating bookings.’,935 )936 );937938 add_settings_field(939 'fh_default_language’,940 'language’,941 array( __CLASS__, ‘render_input_field’ ),942 __FILE__,943 'fh_shortcode_defaults_section’,944 array(945 ‘option_name’ => 'fh_default_language’,946 ‘description’ => 'The two letter code for the language that FareHarbor pages should be overridden to, instead of detecting the user\’s language. (Not recommended.)',947 )948 );949950 add_settings_field(951 'fh_default_full_items’,952 'full_items’,953 array( __CLASS__, ‘render_select_field’ ),954 __FILE__,955 'fh_shortcode_defaults_section’,956 array(957 ‘option_name’ => 'fh_default_full_items’,958 ‘choices’ => array(959 'yes’,960 'no’,961 ),962 ‘description’ => 'Should the Lightframe that is opened include item descriptions and photos? Defaults to “no".’,963 )964 );965966 add_settings_field(967 'fh_default_lightframe’,968 'lightframe’,969 array( __CLASS__, ‘render_select_field’ ),970 __FILE__,971 'fh_shortcode_defaults_section’,972 array(973 ‘option_name’ => 'fh_default_lightframe’,974 ‘choices’ => array(975 'yes’,976 'no’,977 ),978 ‘description’ => 'Choose “yes” to open new bookings inside a Lightframe. Choose “no” to open them in an external page instead. Defaults to “yes".’979 )980 );981982 add_settings_field(983 'fh_default_class’,984 'class’,985 array( __CLASS__, ‘render_input_field’ ),986 __FILE__,987 'fh_shortcode_defaults_section’,988 array(989 ‘option_name’ => 'fh_default_class’,990 ‘description’ => 'The class option only applies to the [lightframe] shortcode.’,991 )992 );993994 add_settings_section(995 'fh_auto_lightframe_section’,996 'Automatic Lightframing’,997 array( __CLASS__, ‘render_fh_auto_lightframe_section_text’ ),998 __FILE__999 );10001001 add_settings_field(1002 'fh_auto_lightframe_enabled’,1003 'Auto Lightframing Activation’,1004 array( __CLASS__, ‘render_input_field’ ),1005 __FILE__,1006 'fh_auto_lightframe_section’,1007 array(1008 ‘option_name’ => 'fh_auto_lightframe_enabled’,1009 ‘type’ => 'checkbox’,1010 ‘label’ => 'Enable auto Lightframing.’,1011 )1012 );10131014 add_settings_section(1015 'fh_buttons_section’,1016 'FareHarbor Buttons’,1017 array( __CLASS__, ‘render_fh_buttons_section_text’ ),1018 __FILE__1019 );10201021 add_settings_field(1022 'fh_buttons_active’,1023 'FH Buttons Activation’,1024 array( __CLASS__, ‘render_input_field’ ),1025 __FILE__,1026 'fh_buttons_section’,1027 array(1028 ‘option_name’ => 'fh_buttons_active’,1029 ‘type’ => 'checkbox’,1030 ‘label’ => 'Load the FH Buttons stylesheet on all pages.’,1031 )1032 );10331034 add_settings_field(1035 'fh_buttons_query’,1036 'Button Colors’,1037 array( __CLASS__, ‘render_input_field’ ),1038 __FILE__,1039 'fh_buttons_section’,1040 array(1041 ‘option_name’ => 'fh_buttons_query’,1042 ‘description’ => 'Provide colors in the format <code>red=ff0000&green=00ff00</code>’,1043 )1044 );10451046 }10471048 public static function validate_settings( $input ) {10491050 // Strip tags and trim whitespace1051 $input = array_map( array( __CLASS__, ‘sanitize’ ), $input );10521053 // Let’s not pollute the DB with empty settings1054 $input = array_filter( $input );10551056 // Shortnames only work when lowercase1057 if ( isset( $input[ ‘fh_default_shortname’ ] ) )1058 $input[ ‘fh_default_shortname’ ] = strtolower( $input[ ‘fh_default_shortname’ ] );10591060 return $input;10611062 }10631064 public static function render_options_page() {10651066 ?>10671068 <div class="wrap">10691070 <h2>FareHarbor Plugin Settings</h2>10711072 <form action="options.php” method="post">10731074 <?php settings_fields( ‘fareharbor_settings’ ); ?>10751076 <?php do_settings_sections( __FILE__ ); ?>10771078 <p class="submit">1079 <input name="Submit” type="submit” class="button-primary” value="<?php esc_attr_e( ‘Save Changes’ ); ?>” />1080 </p>10811082 </form>10831084 </div>10851086 <?php10871088 }10891090 public static function render_fh_about_section_text() {10911092 echo '<p><b>This plugin requires a FareHarbor account to work.</b> FareHarbor provides powerful, intuitive, and highly customizable reservation software for activity and tourism businesses. We can help you enable online booking on your website using this plugin and a whole range of other features. Don\’t have an account? <a href="https://fareharbor.com/join/” target="_blank">Get in touch at fareharbor.com.</a></p><p>If you\’re already with FareHarbor, check out our <a href="https://fareharbor.com/help/setup/wordpress-plugin/” target="_blank">help center article</a> on how to get started with this plugin.</p>’;10931094 }10951096 public static function render_fh_shortcode_defaults_section_text() {10971098 echo '<p>Set default options for the [lightframe], [fareharbor], [itemgrid], and [partners] shortcodes here. The options written in when including a shortcode on a page will always take precedence over the default values entered below. <a href="https://fareharbor.com/help/setup/wordpress-plugin/#setting-defaults” target="_blank">Learn how default options work.</a></p>’;10991100 }11011102 public static function render_fh_auto_lightframe_section_text() {11031104 echo '<p>Open normal links to FareHarbor as Lightframe overlays, even if the [lightframe] shortcode isn’t used.</p>’;11051106 }11071108 public static function render_fh_buttons_section_text() {11091110 echo '<p>FareHarbor can automatically load in a CSS stylesheet which includes classes to make your Lightframe links look like beautifully designed buttons.</p>’;11111112 }11131114 public static function render_input_field( $args ) {1115 // We use this function to render all of our options fields1116 // $args is supplied as the last parameter to each1117 // add_settings_field() call in fareharbor::register_settings()1118 // This function is only run on our settings page in the admin11191120 $type = isset( $args[ ‘type’ ] ) ? $args[ ‘type’ ] : 'text’;11211122 $option_name = $args[ ‘option_name’ ];1123 $value = self::get_option( $option_name, '’, false );11241125 // Attributes all input tags need1126 $attrs = array(1127 ‘name’ => "fareharbor_settings[$option_name]",1128 ‘id’ => $option_name,1129 ‘type’ => $type,1130 );11311132 // Type-specific attributes1133 switch ( $type ) {11341135 case 'text’:1136 $attrs[ ‘size’ ] = '40’;1137 $attrs[ ‘value’ ] = (string) $value;1138 break;11391140 case 'checkbox’:1141 if ( $value )1142 $attrs[ ‘checked’ ] = 'checked’;1143 break;11441145 }11461147 // Merge in attributes supplied in the arguments1148 if ( isset( $args[ ‘attrs’ ] ) )1149 // attributes from the arguments will override the defaults1150 $attrs = $args[ ‘attrs’ ] + $attrs;11511152 // Actually generate the input tag1153 $out = '<input’;1154 foreach ( $attrs as $name => $value )1155 $out .= " $name=\"$value\"";1156 $out .= ' />’;11571158 if ( !empty( $args[ ‘label’ ] ) )1159 $out .= “<label for=\"$option_name\">{$args[ ‘label’ ]}</label>";11601161 if ( !empty( $args[ ‘description’ ] ) )1162 $out .= “<p class=\"description\">{$args[ ‘description’ ]}</p>";11631164 // And print it onto the page1165 echo $out;11661167 }11681169 public static function render_select_field( $args ) {11701171 $option_name = $args[ ‘option_name’ ];1172 $value = self::get_option( $option_name, '’, false );11731174 ?>11751176 <select name="<?php echo “fareharbor_settings[$option_name]" ?>” id="<?php echo $option_name ?>">11771178 <option value="” <?php selected( '’, $value ); ?>></option>11791180 <?php foreach ( $args[ ‘choices’ ] as $choice ) { ?>1181 <option value="<?php echo $choice ?>” <?php selected( $choice, $value ) ?>><?php echo $choice ?></option>1182 <?php } ?>11831184 </select>11851186 <?php11871188 if ( !empty( $args[ ‘description’ ] ) )1189 echo "<p class=\"description\">{$args[ ‘description’ ]}</p>";11901191 }11921193 }