       1: <?php
       2:
       3: /**
       4:  * Class Sidecar_Admin_Page
       5:  */
       6: class Sidecar_Admin_Page {
       7:
       8:   /**
       9:    * @var Sidecar_Plugin_Base
      10:    */
      11:   var $plugin;
      12:
      13:   /**
      14:    * @var null|array Array contains Sidecar_Form objects
      15:    */
      16:   protected $_forms = array();
      17:
      18:   /**
      19:    * @var array
      20:    */
      21:   protected $_tabs = array();
      22:
      23:   /**
      24:    * @var string
      25:    */
      26:   protected $_page_url;
      27:
      28:   /**
      29:    * @var bool
      30:    */
      31:   protected $_is_page_url;
      32:
      33:   /**
      34:    * @var Sidecar_Admin_Tab
      35:    */
      36:   protected $_authentication_tab;
      37:
      38:   /**
      39:    * @var string
      40:    */
      41:   protected $_settings_group_name;
      42:
      43:   /**
      44:    * @var bool
      45:    */
      46:   protected $_initialized = false;
      47:
      48:   /**
      49:    * @var string
      50:    */
      51:   var $parent_slug = 'options-general.php';
      52:
      53:   /**
      54:    * @var string
      55:    */
      56:   var $page_name;
      57:
      58:   /**
      59:    * @var string
      60:    */
      61:   var $page_slug;
      62:
      63:   /**
      64:    * @var string
      65:    */
      66:   var $page_title;
      67:
      68:   /**
      69:    * @var string
      70:    */
      71:   var $menu_title;
      72:
      73:   /**
      74:    * @var string
      75:    */
      76:   var $menu_page;
      77:
      78:   /**
      79:    * @var string
      80:    */
      81:   var $capability_required = 'manage_options';
      82:
      83:   /**
      84:    * @var string One of the built in icons (below), or a custom icon starting with 'https://' or 'https://'
      85:    * @example:
      86:    *    admin, appearance,
      87:    *    comments,
      88:    *    dashboard,
      89:    *    edit, edit-comments, edit-pages,
      90:    *    index,
      91:    *    link, links, link-category, link-manager,
      92:    *    media, ms-admin,
      93:    *    options-general,
      94:    *    page, plugins, post, profile,
      95:    *    settings, site, sitescreen,
      96:    *    themes, tools,
      97:    *    upload, user-edit, users
      98:    */
      99:   var $icon = 'options-general';  // Default
     100:
     101:   /**
     102:    * @var string
     103:    */
     104:   protected $_auth_form = false;
     105:
     106:   /**
     107:    * @param $page_name
     108:    * @param array $args
     109:    */
     110:   function __construct( $page_name, $args = array() ) {
     111:     $this->page_name = $page_name;
     112:
     113:     /**
     114:      * Copy properties in from $args, if they exist.
     115:      */
     116:     foreach( $args as $property => $value )
     117:       if ( property_exists(  $this, $property ) )
     118:         $this->$property = $value;
     119:
     120:     /**
     121:      * Check $this->plugin first so we don't couple these if we don't have to.
     122:      */
     123:     if ( $this->plugin instanceof Sidecar_Plugin_Base ) {
     124:       if ( ! $this->page_title )
     125:         $this->page_title = $this->plugin->plugin_label;
     126:
     127:       if ( ! $this->menu_title )
     128:         $this->menu_title = $this->plugin->plugin_label;
     129:
     130:       if ( ! $this->page_slug )
     131:         $this->page_slug = "{$this->plugin->plugin_slug}-{$page_name}";
     132:     }
     133:
     134:     add_action( 'admin_menu', array( $this, 'admin_menu' ) );
     135:
     136:   }
     137:
     138:   /**
     139:    * @throws Exception
     140:    */
     141:   function initialize() {
     142:     $this->_do_plugin_action( 'initialize_admin_page', $this );
     143:     $current_tab = $this->has_tabs() ? $this->get_current_tab() : false;
     144:     if ( $current_tab && $current_tab->has_forms() ) {
     145:       register_setting( $this->plugin->option_name, $this->plugin->option_name, array( $this, 'filter_postback' ) );
     146:       $plugin = $this->plugin;
     147:       /**
     148:        * Sidecar_Form $form
     149:        */
     150:       foreach( $current_tab->forms as $form ) {
     151:         if ( $plugin->has_form( $form ) ) {
     152:           $form = $this->plugin->get_form( $form );
     153:           $form->admin_page = $this;
     154:           $form->initialize_sections( $this->plugin );
     155:           $form->initialize_buttons( $this->plugin );
     156:           $this->_forms[$form->form_name] = $form;
     157:         }
     158:       }
     159:     }
     160:     $this->_initialized = true;
     161:   }
     162:   /**
     163:    * @param array $input
     164:    *
     165:    * @return array
     166:    */
     167:   function filter_postback( $input ) {
     168:     static $called_already;
     169:     if ( isset( $called_already ) || empty( $_POST ) ) {
     170:       /**
     171:        * When using the Settings API this filter will be called twice when the option needs to be added.
     172:        * This happens because of how WordPress is implemented and not something we can control.
     173:        * IOW, it's a hack but not a hack we can avoid unless WordPress makes changes.
     174:        */
     175:       return $input;
     176:     }
     177:     $unfiltered_input = $input;
     178:     $called_already = true;
     179:     if ( ! current_user_can( 'manage_options' ) ) {
     180:       /**
     181:        * TODO: Verify someone without proper options can actually get here.
     182:        */
     183:       wp_die( __( 'Sorry, you do not have sufficient priviledges.' ) );
     184:     }
     185:
     186:     $this->_do_plugin_action( 'initialize_postback' );
     187:
     188:     /**
     189:      * Get the array that contains names of 'plugin', 'page', 'tab', 'form' and 'settings'
     190:      * as well as special 'clear' and 'reset' for clearing and resetting the form respectively.
     191:      */
     192:     $post_values = $_POST[$_POST['option_page']];
     193:     $this->plugin->set_current_admin_page( $this );
     194:     $form = $this->plugin->get_form( $post_values['_sidecar_form_meta']['form'] );
     195:     $this->plugin->set_current_form( $form );
     196:
     197:     $form_values = $input[$form->form_name];
     198:     /**
     199:      * Check with the API to see if we are authenticated
     200:      * @var RESTian_Client $api
     201:      */
     202:     $api = $this->plugin->get_api();
     203:     if ( $api && ( $this->is_authentication_tab() || ! $this->has_tabs() ) && $form == $this->get_auth_form() ) {
     204:       if ( ! $api->is_credentials( $form_values ) ) {
     205:         add_settings_error( $this->plugin->option_name, 'sidecar-no-credentials', $api->get_message() );
     206:       } else {
     207:         /**
     208:          * @var RESTian_Response
     209:          */
     210:         $response = $api->authenticate( $form_values );
     211:         if ( $response->has_error() ) {
     212:           $form_values['authenticated'] = false;
     213:           if ( ! ( $message = $response->get_error()->message ) ) {
     214:             $message = 'Please try again.';
     215:           };
     216:           add_settings_error( $this->plugin->option_name, 'sidecar-not-authenticated', __( "Authentication Failed. {$message}", 'sidecar' ) );
     217:         } else {
     218:           $form_values = array_merge( $form_values, $response->grant );
     219:           $form_values['authenticated'] = true;
     220:           $message = $this->_apply_plugin_filter( 'filter_authentication_success_message', __( 'Authentication successful. Settings saved.', 'sidecar' ) );
     221:           if ( $message )
     222:             add_settings_error( $this->plugin->option_name, 'sidecar-authenticated', $message, 'updated' );
     223:         }
     224:       }
     225:     }
     226:     //$this->plugin->set_api( $api );
     227:
     228:     if ( isset( $post_values['action']['clear'] ) ) {
     229:       $form_values = $form->get_empty_field_values();
     230:       $message = __( 'Form values cleared.%s%sNOTE:%s Your browser may still be displaying values from its cache but this plugin has indeed cleared these values.%s', 'sidecar' );
     231:       add_settings_error( $this->plugin->option_name, "sidecar-clear", sprintf( $message, "<br/><br/>&nbsp;&nbsp;&nbsp;", '<em>', '</em>', '<br/><br/>' ), 'updated' );
     232:     } else if ( isset( $post_values['action']['reset'] ) ) {
     233:       $form_values = $this->plugin->get_current_form()->get_default_settings_values();
     234:       add_settings_error( $this->plugin->option_name, 'sidecar-reset', __( 'Defaults reset.', 'sidecar' ), 'updated' );
     235:     } else {
     236:       $form_values = array_map( 'rtrim', (array)$form_values );
     237:       add_filter( $action_key = "pre_update_option_{$this->plugin->option_name}", array( $this->plugin, '_pre_update_option' ), 10, 2 );
     238:       /**
     239:        * @todo How to signal a failed validation?
     240:        */
     241:       $form_values = $this->_apply_plugin_filter( 'validate_settings_values', $form_values, $form );
     242:
     243:       /**
     244:        * Esnure that all fields have a value in the $form_values array.
     245:        * Checkboxes and radio buttons might not.
     246:        */
     247:       $form_values = $form->ensure_default_values( $form_values );
     248:
     249:       /**
     250:        * @var Sidecar_Field $field
     251:        */
     252:       foreach( $form->get_fields() as $field_name => $field ) {
     253:         $validation_options = false;
     254:         if ( $field->field_allow_html )
     255:           $form_values[$field_name] = htmlentities( $form_values[$field_name] );
     256:             /**
     257:              * Default to FILTER_SANITIZE_STRING if ['validator'] not set.
     258:              */
     259:             if ( $field->field_options ) {
     260:           $validated_value = isset( $field->field_options[$form_values[$field_name]] ) ? $form_values[$field_name] : false;
     261:         } else if ( isset( $field->field_validator['filter'] ) ) {
     262:            $validated_value = filter_var( $form_values[$field_name], $field->field_validator['filter'] );
     263:            if ( isset( $field->field_validator['options'] ) ) {
     264:             $validation_options = $field->field_validator['options'];
     265:            }
     266:         } else {
     267:           $validator = $field->field_validator ? $field->field_validator : FILTER_SANITIZE_STRING;
     268:           $validated_value = filter_var( $form_values[$field_name], $validator );
     269:         }
     270:         $validated_value = $this->_apply_plugin_filter( "sanitize_setting_{$field_name}", $validated_value, $field, $form );
     271:         if ( $validation_options || $validated_value != $form_values[$field_name] ) {
     272:           if ( ! $validation_options ) {
     273:             add_settings_error( $this->plugin->option_name, 'sidecar-value', sprintf(
     274:               __( 'Please enter a valid value for "%s."', 'sidecar' ), $field->field_label
     275:             ));
     276:           } else {
     277:             if ( isset( $validation_options['min'] ) && $validation_options['min'] > intval( $form_values[$field_name] ) ) {
     278:               add_settings_error( $this->plugin->option_name, 'sidecar-min', sprintf(
     279:                 __( 'Please enter a value greater than or equal to %d for "%s."', 'sidecar' ),
     280:                   $validation_options['min'],
     281:                   $field->field_label
     282:               ));
     283:             }
     284:             if ( isset( $validation_options['max'] ) && $validation_options['max'] < intval( $form_values[$field_name] ) ) {
     285:               add_settings_error( $this->plugin->option_name, 'sidecar-max', sprintf(
     286:                 __( 'Please enter a value less than or equal to %d for "%s."', 'sidecar' ),
     287:                   $validation_options['max'],
     288:                   $field->field_label
     289:               ));
     290:               $continue = true;
     291:             }
     292:           }
     293:         }
     294:       }
     295:     }
     296:
     297:     $form_values = $this->_apply_plugin_filter( $method_name = "process_form_{$form->form_name}", $form_values );
     298:     if ( method_exists( $this->plugin, $method_name ) ) {
     299:       /**
     300:        * This presumes that "process_form_{$form->form_name}" uses the $api.
     301:        * We may need to make it a bit more generic, i.e. allow setting a message on the plugin
     302:        * and then our process form would need to set the plugin's message.
     303:        */
     304:       if ( ! empty( $api->response->message ) ) {
     305:         $message_type = $api->response->has_error() ? 'error' : 'updated';
     306:         add_settings_error( $this->plugin->option_name, "sidecar-form-processed-{$form->form_name}", $api->response->message, $message_type );
     307:       }
     308:     }
     309:
     310:
     311:     $input[$form->form_name] = $form_values;
     312:
     313:     $input = $this->_apply_plugin_filter( 'filter_postback', $input );
     314:
     315:     $postback_info = (object)array(
     316:       'admin_page' => $this,
     317:       'form' => $form,
     318:       'form_values' => $form_values,
     319:       'input' => $input,
     320:       'unfiltered' => $unfiltered_input
     321:     );
     322:
     323:     $this->_do_plugin_action( "set_postback_{$form->form_name}_{$this->page_name}_message", $postback_info );
     324:     $this->_do_plugin_action( 'set_postback_message', $this, $form, $postback_info );
     325:
     326:     return $input;
     327:   }
     328:
     329:
     330:   /**
     331:    * @return array
     332:    */
     333:   function get_auth_credentials() {
     334:     return $this->get_auth_form()->get_settings_values();
     335:   }
     336:
     337:   /**
     338:    * @return Sidecar_Form
     339:    */
     340:   function get_auth_form() {
     341:     return $this->plugin->get_form( $this->_auth_form );
     342:   }
     343:
     344:   /**
     345:    * @param string|Sidecar_Form $form
     346:    */
     347:   function set_auth_form( $form ) {
     348:     if ( is_string( $form ) ) {
     349:       $this->_auth_form = $form;
     350:     } else if ( isset( $form->form_name ) ) {
     351:       $this->_auth_form = $form->form_name;
     352:     } else if ( WP_DEBUG ) {
     353:       $message = __( '%s->set_auth_form() must be passed a string, an array with a \'form_name\' element or an object with a \'form_name\' property.', 'sidecar' );
     354:       trigger_error( sprintf( $message, $this->plugin_class ) );
     355:     }
     356:   }
     357:
     358:   /**
     359:    * @param string $tab_slug
     360:    * @param string $tab_text
     361:    * @param array $args
     362:    */
     363:   function add_tab( $tab_slug, $tab_text, $args = array() ) {
     364:     $args['plugin'] = $this->plugin;
     365:     if ( ! $this->_auth_form && 'account' == $tab_slug )
     366:       $this->_auth_form = 'account';
     367:     $this->_tabs[$tab_slug] = $tab = new Sidecar_Admin_Tab( $tab_slug, $tab_text, $args );
     368:     }
     369:
     370:   /**
     371:    * @return Sidecar_Admin_Tab
     372:    */
     373:   function get_default_tab() {
     374:     return reset( $this->_tabs );
     375:   }
     376:
     377:   /**
     378:    * Test to see if the current tab is the authentication tab
     379:    *
     380:    * @return bool
     381:    */
     382:   function is_authentication_tab() {
     383:     $tab = $this->get_authentication_tab();
     384:     return is_object( $tab ) && $tab === $this->get_current_tab();
     385:   }
     386:   /**
     387:    * @return Sidecar_Admin_Tab
     388:    */
     389:   function get_authentication_tab() {
     390:     if ( ! $this->_authentication_tab ) {
     391:       /**
     392:        * @var Sidecar_Admin_Tab $tab
     393:        */
     394:       foreach( $this->_tabs as $tab ) {
     395:         if ( in_array( $this->_auth_form, $tab->forms ) ) {
     396:           $this->_authentication_tab = $tab;
     397:           break;
     398:         }
     399:       }
     400:     }
     401:     return $this->_authentication_tab;
     402:   }
     403:
     404:   /**
     405:    * Filters a value by calling a method in the plugin.
     406:    *
     407:    * Captures whatever additional parameters and passes them on.
     408:    *
     409:    * @param string $method
     410:    *
     411:    * @return bool|mixed
     412:    */
     413:   protected function _apply_plugin_filter( $method ) {
     414:     $args = func_get_args();
     415:     array_shift( $args );
     416:     $result = $args[0];
     417:     if ( method_exists( $this->plugin, $method ) )
     418:       $result = call_user_func_array( array( $this->plugin, $method ), $args );
     419:     return $result;
     420:   }
     421:   /**
     422:    * Executes an action by calling a method in the plugin.
     423:    *
     424:    * Captures whatever additional parameters and passes them on.
     425:    *
     426:    * @param string $method
     427:    *
     428:    * @return bool|mixed
     429:    */
     430:   protected function _do_plugin_action( $method ) {
     431:     $result = false;
     432:     $args = func_get_args();
     433:     array_shift( $args );
     434:     if ( method_exists( $this->plugin, $method ) )
     435:       $result = call_user_func_array( array( $this->plugin, $method ), $args );
     436:     return $result;
     437:   }
     438:   /**
     439:    * Retrieves a value from the plugin.
     440:    *
     441:    * Captures whatever additional parameters and passes them on.
     442:    *
     443:    * @param string $method
     444:    *
     445:    * @return bool|mixed
     446:    */
     447:   protected function _get_plugin_value( $method ) {
     448:     $args = func_get_args();
     449:     return call_user_func_array( array( $this, '_do_plugin_action' ), $args );
     450:   }
     451:
     452:   /**
     453:    *
     454:    */
     455:   function admin_menu() {
     456:
     457:     if ( $this->is_postback_update() )
     458:       return;
     459:
     460:     if ( $this->is_page_url() ) {
     461:       /**
     462:        * Call the plugin's $this->initialize_admin_page() method, if it exists.
     463:        */
     464:       $this->initialize();
     465:
     466:       /**
     467:        * Call the plugin's $this->verify_current_tab() method, if it exists.
     468:        * Return true if all is okay and you don't want to run the default,
     469:        * return false if you want to run the default, or
     470:        * redirect and exit inside the method.
     471:        */
     472:       $this->_get_plugin_value( 'verify_current_tab', $this );
     473:     }
     474:
     475:     /**
     476:      * Add in menu option, if one doesn't already exist
     477:      */
     478:     $this->menu_page = add_submenu_page(
     479:           $this->parent_slug,
     480:       $this->page_title,
     481:       $this->menu_title,
     482:       $this->capability_required,
     483:       $this->page_slug,
     484:       array( $this, 'the_page' )
     485:     );
     486:
     487:     add_action( "admin_print_styles-{$this->menu_page}", array( $this, 'admin_print_styles' ));
     488:     }
     489:
     490:   /**
     491:    *
     492:    */
     493:   function admin_print_styles() {
     494:     $plugin = $this->plugin;
     495:       $localfile = 'css/admin-style.css';
     496:       $filepath = "{$plugin->plugin_path}/{$localfile}";
     497:       if ( file_exists( $filepath ) ) {
     498:           wp_enqueue_style( "{$plugin->plugin_name}_admin_styles", plugins_url( $localfile, $plugin->plugin_file ) );
     499:      }
     500:     }
     501:
     502:   /**
     503:    * @todo Call this from HEAD instead of from here.
     504:    */
     505:   function the_css() {
     506:     $css = $this->_get_plugin_value( 'get_admin_page_css', $this );
     507:     if ( $css ) {
     508:       $css_html =<<<HTML
     509: <style type="text/css">
     510: {$css}
     511: </style>
     512: HTML;
     513:       echo $css_html;
     514:     }
     515:   }
     516:   /**
     517:      * Displays admin page
     518:      */
     519:     function the_page() {
     520:      /**
     521:       * @todo Call this from HEAD instead of from here.
     522:       */
     523:     $this->the_css();
     524:     $tab = $this->get_current_tab();
     525:     $tab_class= $tab ? " tab-{$tab->tab_slug}" : false;
     526:     $id = rtrim( "{$this->page_slug}-" . ltrim( $tab->tab_slug ), '-' );
     527:       echo <<<HTML
     528: \n<div id="{$id}" class="wrap {$this->plugin->css_base}-admin {$this->page_name}-page{$tab_class}">
     529: HTML;
     530:       $this->the_icon();
     531:     $this->the_title_and_tabs( $tab );
     532:     $this->the_page_content();
     533:       echo "\n" . '</div>';
     534:     }
     535:
     536:   /**
     537:    * @param string $tab
     538:    * @param string $content_type
     539:    * @param array $args
     540:    */
     541:   function the_tab_specific_content( $tab, $content_type, $args = array() ) {
     542:     $args = wp_parse_args( $args, array( 'wrap' => true ) );
     543:     $content = false;
     544:     if ( ! $tab || ! isset( $tab->{$content_type} ) )
     545:       return;
     546:     if ( is_string( $tab->{$content_type} ) ) {
     547:       $content = $tab->{$content_type};
     548:     } else {
     549:       $handler = $tab->{$content_type};
     550:       if ( ! is_callable( $tab->{$content_type} ) ) {
     551:         $method = "the_{$this->page_name}_{$tab->tab_slug}_tab_{$content_type}";
     552:         $handler = method_exists( $this->plugin, $method ) ? array( $this->plugin, $method ) : false;
     553:       }
     554:       if ( $handler ) {
     555:         ob_start();
     556:         call_user_func( $handler, $this, $tab );
     557:         $content = ob_get_clean();
     558:       }
     559:     }
     560:     if ( $content ) {
     561:       if ( $args['wrap'] ) {
     562:         $content_type_slug = str_replace( '_', '-', $content_type );
     563:         $content =<<< HTML
     564: <div id="tab-{$content_type_slug}">
     565: {$content}
     566: </div>
     567: HTML;
     568:       }
     569:       echo $content;
     570:     }
     571:   }
     572:   /**
     573:    *
     574:    */
     575:   function the_page_content() {
     576:     echo <<<HTML
     577: <div id="admin-content">
     578: HTML;
     579:     /**
     580:      * @var bool|Sidecar_Admin_Tab
     581:      */
     582:     $current_tab = $this->has_tabs() ? $this->get_current_tab() : false;
     583:
     584:     $this->the_tab_specific_content( $current_tab, 'before_page_title', array( 'wrap' => false ) );
     585:
     586:     if ( $current_tab && $current_tab->page_title ) {
     587:       echo "<h1 class=\"admin-page-title\">";
     588:       echo "\n" . $current_tab->page_title;
     589:       echo "\n" . '</h1>';
     590:     }
     591:
     592:     $this->the_tab_specific_content( $current_tab, 'before_content' );
     593:
     594:     if ( $current_tab && $current_tab->tab_handler ) {
     595:       $handler =  $current_tab->tab_handler;
     596:       if ( ! is_callable( $handler ) ) {
     597:         $method = $handler;
     598:         if ( is_array( $method ) ){
     599:           $method = ( is_string( $handler[0] ) ? "{$handler[0]}::" : get_class( $handler[0] ) .'->' ) . $handler[1];
     600:         }
     601:         $message = __( '%s provided as %s for admin tab %s of admin page %s is not a valid callable.', 'sidecar' );
     602:         Sidecar::show_error( $message,
     603:           "<strong><code>{$method}()</code></strong>",
     604:           "<strong><code>tab_handler</code></strong>",
     605:           "<strong><em>\"{$current_tab->tab_slug}\"</em></strong>",
     606:           "<strong><em>\"{$this->page_name}\"</em></strong>"
     607:           );
     608:       }
     609:     } else {
     610:       $page_name = str_replace( '-', '_', $this->page_name );
     611:       $handler = array( $this->plugin, "the_{$page_name}_admin_page" );
     612:       if ( $current_tab ) {
     613:         $tab_slug = str_replace( '-', '_', $current_tab->tab_slug );
     614:         $tab_handler = array( $this->plugin, "the_{$this->page_name}_{$tab_slug}_tab" );
     615:         /**
     616:          * Fallback to page handler if tab handler is not callable
     617:          */
     618:         $handler = is_callable( $tab_handler ) ? $tab_handler : $handler;
     619:       }
     620:       if ( ! is_callable( $handler ) && $this->plugin->has_form( $current_tab->tab_slug ) ) {
     621:         /*
     622:          * If we have no handler but do have a form with same name as the tab slug, show it.
     623:          */
     624:         $handler = array( $this->plugin->get_form( $current_tab->tab_slug ), 'the_form' );
     625:       }
     626:       if ( ! is_callable( $handler ) ) {
     627:         if ( isset( $tab_handler ) )
     628:           /**
     629:            * If it was a tab then report the more specific function as the one we are missing
     630:            */
     631:           $handler = $tab_handler;
     632:         $message = __( 'No method named %s defined yet for %s.', 'sidecar' );
     633:         Sidecar::show_error( $message,
     634:           "<strong><code>{$handler[1]}()</code></strong>",
     635:           "<strong><code>{$this->plugin->plugin_class}</code></strong>"
     636:           );
     637:       }
     638:     }
     639:     if ( is_callable( $handler ) ) {
     640:       $this->_do_plugin_action( 'initialize_tab', $current_tab, $this );
     641:       call_user_func( $handler, $this );
     642:     }
     643:
     644:     $this->the_tab_specific_content( $current_tab, 'after_content' );
     645:     echo '</div>';
     646:   }
     647:   /**
     648:    * Displays the icon for the plugin page
     649:    *
     650:    * @todo Research to see if we need to support something other than icon32
     651:    *
     652:    */
     653:   function the_icon() {
     654:     if ( $icon_html = $this->_get_plugin_value( 'get_icon_html', $this ) ) {
     655:       $icon_html = <<<HTML
     656: <div class="icon32">{$icon_html}</div>
     657: HTML;
     658:     } else if ( $this->icon ) {
     659:       /**
     660:        * $this->icon contains a URL for an image.
     661:        */
     662:       if ( preg_match( '#^https?://#', $this->icon, $m ) ) {
     663:         list( $width, $height ) = explode( 'x', $this->_apply_plugin_filter( 'get_icon_dimensions', '36x34' ) );
     664:         $icon_html_fragment =<<<HTML
     665: ><img height="{$height}" style="background:none;" width="{$width}" src="{$this->icon}>
     666: HTML;
     667:       } else {
     668:         /**
     669:          * This means $this->icon contains an idea value such as
     670:          */
     671:         $icon_html_fragment = " id=\"icon-{$this->icon}\"><br/>";
     672:       }
     673:       /**
     674:        * Note that there is no trailing ">" on the first <div>.
     675:        * Instead, the $icon_html_fragment contains it.
     676:        */
     677:       $icon_html = <<<HTML
     678: <div class="icon32"{$icon_html_fragment}</div>
     679: HTML;
     680:     }
     681:     if ( $icon_html )
     682:       echo $icon_html;
     683:   }
     684:   /**
     685:    *
     686:    */
     687:   function get_tabs() {
     688:       return $this->_tabs;
     689:     }
     690:
     691:   /**
     692:    * @param string $tab_name
     693:    *
     694:    * @return bool|Sidecar_Admin_Tab
     695:    */
     696:   function get_tab( $tab_name ) {
     697:       return isset( $this->_tabs[$tab_name] ) ? $this->_tabs[$tab_name] : false;
     698:     }
     699:   /**
     700:    *
     701:    */
     702:   function has_tabs() {
     703:       return 0 < count( $this->_tabs );
     704:     }
     705:
     706:   /**
     707:    * Display the row of tabs at the top of a page with the <h2> tab wrapper element
     708:    */
     709:   function the_title_and_tabs() {
     710:     if ( $this->page_title || $this->has_tabs() ) {
     711:       echo "\n" . '<h2 class="nav-tab-wrapper">';
     712:       if ( $this->page_title )
     713:         echo "\n<span class=\"admin-page-title\">{$this->page_title}</span>";
     714:       if ( $this->has_tabs() )
     715:         echo "\n" . $this->get_tabs_html();
     716:       echo "\n" . '</h2>';
     717:     }
     718:     }
     719:
     720:   /**
     721:    * Returns the tabs as a block of HTML for display at the top of the admin page.
     722:    *
     723:    * @return string
     724:    */
     725:   function get_tabs_html() {
     726:       return implode( "\n", $this->get_tab_links_html() );
     727:     }
     728:
     729:   /**
     730:    * Returns an array of HTML for each tabs for display at the top of the admin page.
     731:    *
     732:    */
     733:   function get_tab_links_html() {
     734:     $links_html = array();
     735:     $current_tab = $this->get_current_tab();
     736:     if ( $current_tab ) {
     737:       foreach ( $this->get_tabs() as $tab_slug => $tab ) {
     738:         $class = ( $tab_slug == $current_tab->tab_slug ) ? ' nav-tab-active' : '';
     739:         $url = $this->get_tab_url( $tab_slug );
     740:         $links_html[] =<<<HTML
     741:   <a class="nav-tab{$class}" href="{$url}">{$tab->tab_text}</a>
     742: HTML;
     743:       }
     744:     }
     745:     return $links_html;
     746:   }
     747:
     748:
     749:   /**
     750:    * @return string|void
     751:    */
     752:   function get_page_url() {
     753:     return $this->get_tab_url(null);
     754:     }
     755:
     756:   /**
     757:    * @return string|void
     758:    */
     759:   /**
     760:    * @param null|bool|string|Sidecar_Admin_Tab $tab
     761:    * @param string $text
     762:    * @param bool $blank
     763:    *
     764:    * @return string|void
     765:    */
     766:   function get_tab_link( $tab, $text, $blank = false ) {
     767:     $url = $this->get_tab_url( $tab );
     768:     if ( $blank )
     769:       $blank = ' target="_blank"';
     770:     return <<<HTML
     771: <a{$blank} href="{$url}">{$text}</a>
     772: HTML;
     773:     }
     774:
     775:   /**
     776:    * @param null|bool|string|Sidecar_Admin_Tab $tab If null is passed then don't add a default tab.
     777:    * @return string|void
     778:    */
     779:   function get_tab_url( $tab = false ) {
     780:     if ( ! $this->_initialized ) {
     781:       $message = __( '%s->get_page_url() cannot be called prior to %s->initialize_admin_page() being called.', 'sidecar' );
     782:       Sidecar::show_error( $message, __CLASS__, $this->plugin->plugin_class );
     783:     }
     784:
     785:     if ( $tab instanceof Sidecar_Admin_Tab )
     786:       $tab = $tab->tab_slug;
     787:     if ( $this->has_tabs() ) {
     788:       if ( isset( $this->_page_url[$tab] ) ) {
     789:         $url = $this->_page_url[$tab];
     790:       } else {
     791:         if ( false === $tab )
     792:           $tab = $this->get_default_tab()->tab_slug;
     793:         $url = $this->_page_url[$tab] = $this->get_base_page_url() . ( is_null($tab) ? '' : "&tab={$tab}" );
     794:       }
     795:       ;
     796:     } else {
     797:       if ( isset( $this->_page_url ) ) {
     798:         $url = $this->_page_url;
     799:       } else {
     800:         $url = $this->_page_url = $this->get_base_page_url();
     801:       }
     802:     }
     803:     return $url;
     804:     }
     805:
     806:   /**
     807:    * @return string|void
     808:    */
     809:   function get_base_page_url() {
     810:     return admin_url( "{$this->parent_slug}?page={$this->page_slug}" );
     811:   }
     812:
     813:   /**
     814:      * Check if the passed $tab variable matches the URL's tab parameter.
     815:      *
     816:      * @param string $tab
     817:      * @return bool
     818:      */
     819:     function is_current_tab( $tab ) {
     820:         /**
     821:          * IF the the current page has a valid tab,
     822:          * AND the URL's 'tab' parameter matches the function's $tab parameter
     823:          * THEN *YES*, it's the tab specified
     824:          */
     825:         return $this->is_current_tab_valid() && $tab == $_GET['tab'];
     826:     }
     827:
     828:   /**
     829:      * Check if the passed $tab variable matches the URL's tab parameter.
     830:      *
     831:      * @return Sidecar_Admin_Tab
     832:      */
     833:     function get_current_tab() {
     834:       static $current_tab;
     835:       if ( ! isset( $current_tab ) ) {
     836:       $tab_slug = false;
     837:       if ( isset( $_GET['tab'] ) ) {
     838:          $tab_slug = $_GET['tab'];
     839:       } else if ( isset( $_POST['option_page'] ) && isset( $_POST[$_POST['option_page']]['_sidecar_form_meta']['tab'] )) {
     840:         /*
     841:          * This is used during HTTP postback from an admin form.
     842:          * The <input type="hidden" name="option_page"> added by settings_fields() and
     843:          * referencing <input type="hidden" name="{option_page}[tab]"> generated by $form->get_html()
     844:          */
     845:         $tab_slug = $_POST[$_POST['option_page']]['_sidecar_form_meta']['tab'];
     846:       }
     847:       $current_tab = $tab_slug && isset( $this->_tabs[$tab_slug] ) ? $this->_tabs[$tab_slug] : reset( $this->_tabs );
     848:     }
     849:    return $current_tab;
     850:   }
     851:
     852:   /**
     853:      * Check if the passed $tab variable matches the URL's tab parameter.
     854:      *
     855:      * @param string|Sidecar_Admin_Tab $tab
     856:      * @return bool
     857:      */
     858:     function has_tab( $tab ) {
     859:       if ( isset( $tab->tab_slug ) ) {
     860:         $tab = $tab->tab_slug;
     861:      }
     862:         return isset( $this->_tabs[$tab] );
     863:     }
     864:
     865:   /**
     866:      *
     867:      * @return int
     868:      */
     869:     function tab_count() {
     870:         return count( $this->_tabs );
     871:     }
     872:
     873:   /**
     874:    * Validates to ensure that we have a URL tab parameter that is one of the valid tabs.
     875:    *
     876:    * @return bool
     877:    */
     878:   function is_current_tab_valid() {
     879:     return $this->has_tab( $this->get_current_tab() ) && $this->is_page_url();
     880:   }
     881:
     882:   /**
     883:      * Check to see if we are on the admin URL for this plugin.
     884:      *
     885:      * @return bool
     886:      */
     887:     function is_page_url() {
     888:         if ( ! isset( $this->_is_page_url ) ) {
     889:             $base_url = $this->get_base_page_url();
     890:             /*
     891:              * Test to see if the left-most characters of this URL match the base URL, i.e. this is match
     892:              *
     893:              *  base_url = https://example.com/wp/wp-admin/options-general.php?page=my-plugin-settings
     894:              *  this_url = https://example.com/wp/wp-admin/options-general.php?page=my-plugin-settings OR
     895:              *  this_url = https://example.com/wp/wp-admin/options-general.php?page=my-plugin-settings&tab=support
     896:              *
     897:              * This is NOT a match:
     898:              *
     899:              *  base_url = https://example.com/wp/wp-admin/options-general.php?page=my-plugin-settings
     900:              *  this_url = https://example.com/wp/wp-admin/options-general.php OR
     901:              *  this_url = https://example.com/wp/wp-admin/edit-comments.php OR
     902:              *  this_url = https://example.com/wp/wp-admin/
     903:              *
     904:              */
     905:       $this->_is_page_url = $base_url == substr( Sidecar::this_url(), 0, strlen( $base_url ) );
     906:         }
     907:         return $this->_is_page_url;
     908:     }
     909:
     910:   /**
     911:    * @return bool
     912:    */
     913:   function is_postback_update() {
     914:     global $pagenow;
     915:     $is_postback_update = false;
     916:     if ( isset( $_POST['action'] ) && 'update' == $_POST['action'] && 'options.php' == $pagenow ) {
     917:       $this->initialize();
     918:       $is_postback_update = isset( $_POST[$this->plugin->option_name] );
     919:     }
     920:     return $is_postback_update;
     921:   }
     922:
     923:   /**
     924:      * Check to make sure we are on the right tab.
     925:      *
     926:      * Redirect if we are not on the right tab based on authentication status or invalid tab,
     927:      * OR return if not even on this plugin's Admin URL
     928:      * Register an error message for later display if not authenticated.
     929:      *
     930:      * @return bool Returns true if we are on a verified tab, false or URL redirect otherwise.
     931:      */
     932:     function verify_current_tab() {
     933:      /*
     934:       * If we have no tabs trying to verify is a moot point. Punt.
     935:       */
     936:     if ( ! $this->has_tabs() )
     937:       return true;
     938:
     939:     $needs_grant = $this->plugin->needs_grant();
     940:     $has_grant = $this->plugin->has_grant();
     941:
     942:         if ( ! $this->is_current_tab_valid() ) {
     943:             /**
     944:              * If we don't have a valid tab and we are using an API, redirect to first tab if we have an grant or authentication tab if not.
     945:        *
     946:        * We redirect to avoid having multiple URLs mean the same thing which would not be optimal for bookmarking, caching, etc.
     947:              */
     948:             if ( $needs_grant && $has_grant ) {
     949:                 /**
     950:                  * If authenticated we redirect with a "301 - This URL has changed" status code so the browser can know
     951:                  * to go to 'usage' whenever is sees this URL and avoid the round trip next time.
     952:                  */
     953:                 wp_safe_redirect( $this->get_tab_url( $this->get_default_tab()->tab_slug ), 301 );
     954:         exit;
     955:             } else if ( $needs_grant && $auth_tab = $this->get_authentication_tab() ) {
     956:                 /**
     957:                  * If not authenticated we redirect with a "302 - This URL has moved temporarily" status code
     958:                  * because normally we'd want to go to usage, so don't cause browser to thing this URL w/o a
     959:                  * valid tab should always go to 'account.'
     960:                  */
     961:                 wp_safe_redirect( $this->get_tab_url( $auth_tab->tab_slug ), 302 );
     962:         exit;
     963:             }
     964:         } else if ( $needs_grant && ! $has_grant ) {
     965:             /**
     966:              * If we are using an API but we don't have a grant
     967:              */
     968:       $auth_tab = $this->get_authentication_tab();
     969:        /**
     970:         * If there is a tab and we are not already on the authentication tab
     971:         */
     972:        if ( $auth_tab && ( ! isset( $_GET['tab'] ) || $auth_tab->tab_slug != $_GET['tab'] ) ) {
     973:                 /**
     974:                  * ...and we are NOT on the authentication tab then redirect to the 'account' tab.
     975:                  *
     976:                  * We redirect with a "302 - This URL has moved temporarily" because it's still a good URL and
     977:                  * we want the browser to be happy to return here later.
     978:                  */
     979:                 wp_safe_redirect( $this->get_tab_url( $auth_tab ), 302 );
     980:                 exit;
     981:             } else {
     982:                 /**
     983:                  * ...and we ARE on the authentication tab then prepare a "Need to authenticate" message for later display.
     984:                  */
     985:                 add_settings_error(
     986:                     $this->plugin->plugin_slug, // @todo Switch to $this->form_name,
     987:                     'need-info',
     988:                     __( 'You must have an account to use this plugin.  Please enter your credentials.', 'sidecar' )
     989:                 );
     990:             }
     991:         }
     992:         return true;
     993:     }
     994:   /**
     995:    * @param $property_name
     996:    * @return bool|string
     997:    */
     998:   function __get( $property_name ) {
     999:     $value = false;
    1000:     if ( preg_match( '#^(.+?)_tab_url$#', $property_name, $match ) && $this->has_tab( $match[1] ) ) {
    1001:       /**
    1002:        * Allows magic property syntax for any registered URL
    1003:        * @example: $this->foobar_url calls $this-get_url( 'foobar' )
    1004:        * Enables embedding in a HEREDOC or other doublequoted string
    1005:        * without requiring an intermediate variable.
    1006:        */
    1007:       $value = call_user_func( array( $this, "get_tab_url" ), $match[1] );
    1008:     } else {
    1009:       Sidecar::show_error( 'No property named %s on %s class.', $property_name, get_class( $this ) );
    1010:     }
    1011:     return $value;
    1012:   }
    1013:
    1014: }
    1015: