Файловый менеджер - Редактировать - /home/harasnat/www/labour/wp-content/plugins/events-manager/classes/em-list-table.php
Назад
<?php namespace EM { use WP_List_Table; // WP_List_Table is not loaded automatically so we need to load it in our application if( ! class_exists( 'WP_List_Table' ) ) { require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' ); } /** * Create a new table class that will extend the WP_List_Table */ class List_Table extends WP_List_Table { /** * Results of given table search. * @var array[]|\EM_Object[] */ public $items = array(); /** * array containing the columns to display, the keys and values would be the column key, information about the column is referred to in $cols_template * @var array */ public $cols = array(); /** * Asoociative array of available collumn keys and corresponding headers, which will be used to display this table of items * @var array */ public $cols_template = array(); public $cols_template_groups = array(); public static $cols_allowed_html = array(); /** * Object context we're viewing bookings in relation to, such as events, locations, tickets, etc. used for saving view settings like columns and determining what data to show * @var string */ public $context; public $context_views = array(); /** * Link/Data for available actions on each row for this table. Populated by self::get_action_data() method. * @var array */ public $action_data = array(); public static $basename = 'em_list_table'; public static $template_component_name = 'list-table'; public static $export_action = 'em_list_table_export'; public static $export_delimiter = ','; /** * Class is in the process of exporting when true. Assume that nonce has been verified once set to true. * @var bool */ public static $exporting = false; /** * Added to the form wrapping the actual list table, use this for unique class names * @var string */ public static $form_class = ''; /** * Set to the base name used for a class or unique ID * @var string */ public $id = 'em-list-table'; /** * Set to a unique id made up of $this->id dash rand() int during __construct * @var string */ public $uid = 'em-list-table-0'; /** * Maximum number of rows to show * @var int */ public $limit = 20; public $order = 'ASC'; /** * Field to order by default, overriding classes must set this! * @var string */ public $orderby; public $page = 1; public $offset = 0; public $filters = array(); public $per_page = 20; public $total_items = 0; public $per_page_var = 'limit'; /** * Destination output this is intended for, for example html is default, but could be csv, excel, etc. * @var string */ public $format = 'html'; /** * Does this table have checkboxes and, therefore, are bulk actions available? If this is overriden with an id (booking_id, event_id etc.) then it is assumed checkboxes and bulk actions are available. * @var bool */ public $checkbox_id = false; /** * Whether results can be exported. * @var bool */ public static $is_exportable = true; /** * Flag for whether this is a frontend or backend table display. * @var bool */ public static $is_frontend = null; /** * Set to true if current class has filters by extending the extra_tablenav() base method. This allows for adding classes to table without using things like ReflectionClass to figure it out. * @var bool */ public static $has_filters = false; /** * If there are filters, child classes can add the post values here and they can be automatically saved in the settings page, otherwise handle them via the set_default_settings() method. * @var array */ public static $filter_vars = [ 'search' => [ 'param' => 'em_search', 'default' => '', ], 'scope' => [ 'default' => 'future' ] ]; /** * Show or hide filters by default * @var bool */ public static $show_filters = true; /** * Show or prevent repeating tablenav at bottom of table, including pagination * @var bool */ public static $show_bottom_tablenav = true; /** * Show or prevent repeating tablenav actions at bottom of table, such as filters and bulk actions. If set to false, bulk actions and filters are ommitted regardless of other settings. * @var bool */ public static $show_bottom_tablenav_actions = false; /** * Show or prevent repeating bulk actions at bottom of table, including settings, export, show/hide filter and expand/collapse (responsive) icons. * @var bool */ public static $show_bottom_tablenav_bulkactions = true; /** * Show or prevent repeating the filters at bottom of table. * @var bool */ public static $show_bottom_tablenav_extra_tablenav = true; /** * Show or prevent repeating the pagination at bottom of table. * @var bool */ public static $show_bottom_tablenav_pagination = true; /* * Show or hide responsive meta line below the primary column line when in responsive mode */ public static $show_responsive_meta = true; public function __construct ( $args = array() ) { // unique ID $this->uid = $this->id . '-' . rand(1,99999); // load settings, then let query data override $this->load_current_context(); //Set basic vars $this->order = ( !empty($_REQUEST['order']) && ($_REQUEST['order'] == 'DESC' || $_REQUEST['order'] == 'desc') ) ? 'DESC':'ASC'; $this->orderby = ( !empty($_REQUEST['orderby']) ) ? sanitize_sql_orderby($_REQUEST['orderby']): $this->orderby; if( defined('DOING_AJAX') ) { $_GET['order'] = strtolower($this->order); // for WP_List_Table $_GET['orderby'] = $this->orderby; } $this->limit = ( !empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit'])) ? $_REQUEST['limit'] : $this->limit;//Default limit $this->page = ( !empty($_REQUEST['pno']) && is_numeric($_REQUEST['pno']) ) ? $_REQUEST['pno']:1; $_REQUEST['paged'] = $this->page; $this->offset = ( $this->page > 1 ) ? ($this->page-1)*$this->limit : 0; // Basic Vars for List_Table $this->per_page = $this->limit; em_list_table_create_funcitions(); // load the columns now $this->load_columns(); //calculate collumns if post requests if( !empty($_REQUEST['cols']) ){ $this->cols = array(); if( is_array($_REQUEST['cols']) ){ $cols = $_REQUEST['cols']; }else{ $cols = explode(',',$_REQUEST['cols']); } foreach( $cols as $col ){ if( array_key_exists($col, $this->cols_template) ){ $this->cols[$col] = $col; } } } //save collumns depending on context and user preferences if( !empty($_REQUEST['cols']) && !empty($_REQUEST['save']) && wp_verify_nonce($_REQUEST['_emnonce'], static::$basename) ){ //save view settings for next time $settings = $this->set_default_settings(); $this->save_default_settings( $settings ); } // parent parent::__construct( $args ); $this->constructor(); // do action do_action( static::$basename, $this ); // clean any columns from saved views that no longer exist - at this point they shoudl have been added foreach($this->cols as $col_key => $col_name){ if( !is_string($col_name) || !array_key_exists($col_name, $this->cols_template)){ unset($this->cols[$col_key]); } } } /** * Fired after __constructor(), so that child classes can overwrite this method and do more just before the static::$basename action is fired. * @return void */ public function constructor(){} public function load_columns(){ _doing_it_wrong( __FUNCTION__, 'This method must be overwritten in child classes', '6.4.11' ); } public function __get( $prop ) { if ( $prop === 'cols_view' ) { return $this->context; } return parent::__get( $prop ); } public function __set( $prop, $value ) { if ( $prop === 'cols_view' ) { $this->context = $value; } parent::__set( $prop, $value ); } public function __isset ( $prop ) { if ( $prop === 'cols_view' ) { return isset($this->context); } return parent::__isset( $prop ); } /** * Loads the current context and writes the current settings into the object. * * Child classes should overwrite this method to set the context first, or do it before this function is executed in the constructor. * * @return mixed|null */ public function load_current_context() { // get the actual context $settings = $this->get_current_context(); // set things like limit, cols, etc. $this->cols = $settings['cols'] ?? $this->cols; $this->limit = $settings['limit'] ?? $this->limit; $this->filters = $settings['filters'] ?? $this->filters; $this->orderby = $settings['orderby'] ?? $this->orderby; // set default filters - child classes could set them here or after this parent constructor is called foreach ( static::$filter_vars as $filter_key => $filter_vars ) { $default = false; if( is_array($filter_vars) ) { $filter_var = $filter_vars['param'] ?? $filter_key; // allows for further expansion such as special filtering etc. $default = $filter_vars['default'] ?? false; if( !empty($filter_vars['in_array']) ) { $in_array = $filter_vars['in_array']; } } else { if ( is_int($filter_key) ) { $filter_key = $filter_vars; } $filter_var = $filter_vars; } if( isset($_REQUEST[$filter_var]) ) { static::$show_filters = true; } if ( isset($_REQUEST[$filter_var]) ) { // check in_array validation if( !empty($in_array) ) { if( is_array($in_array) && in_array($_REQUEST[$filter_var], $in_array) ) { $this->filters[$filter_key] = $_REQUEST[$filter_var]; } elseif ( !empty($this->{$in_array}) && in_array($_REQUEST[$filter_var], $this->{$in_array}) ) { $this->filters[$filter_key] = $_REQUEST[$filter_var]; } } elseif( !empty($array_key) ) { if ( !empty($this->{$array_key}) && array_key_exists($_REQUEST[$filter_var], $this->{$in_array}) ) { $this->filters[$filter_key] = $_REQUEST[$filter_var]; } elseif ( is_array($array_key) && array_key_exists($_REQUEST[$filter_var], $array_key) ) { $this->filters[$filter_key] = $_REQUEST[$filter_var]; } } else { $this->filters[$filter_key] = sanitize_text_field($_REQUEST[$filter_var]); } } $this->filters[$filter_key] = $this->filters[$filter_key] ?? $default; } return apply_filters( static::$basename . 'load_current_context', $settings, $this); } public function get_current_context() { $default_settings = $this->get_default_settings(); if ( $this->context ) { if ( !empty($default_settings['contexts'][$this->context]) ) { $settings = $default_settings['contexts'][$this->context]; } else { $settings = $default_settings; } $settings['context'] = $this->context; } else { $settings = $default_settings; } if( empty($settings['cols']) || !is_array($settings['cols']) ){ // check if we have template views in place $context_default = $this->get_current_context_default(); $default_cols = $context_default['cols'] ?? $this->cols; // load default cols $settings = array( 'cols' => $default_cols, ); } // merge current settings into default settings without contexts, so base settings fill out any missing settings (if set) $current_context = static::merge_settings( $default_settings, $settings ); unset( $current_context['contexts'] ); return apply_filters( 'em_list_table_get_current_context', $current_context, $this); } public function get_current_context_default() { if( $this->context && !empty( $this->context_views[ $this->context ] ) ) { return $this->context_views[$this->context]; } return false; } public function get_default_settings() { $settings = get_user_meta(get_current_user_id(), static::$basename . '_settings', true ); if( !is_array($settings) ) $settings = array(); return apply_filters( static::$basename . '_get_default_settings', $settings, $this); } /** * Sets default settings for this table under the current context. If $settings is provided, it is considered the context to be saved under. * Otherwise, the default context settings will be loaded and overwritten with currently loaded settings. * * @param $settings * * @return array */ public function set_default_settings( $settings = array() ) { if ( empty($settings) ) $settings = $this->get_current_context(); $settings['cols'] = $this->cols; $settings['limit'] = $this->limit; // if filters are also to be saved, do it here if( !empty($_REQUEST['save_filters']) ){ // go through filters array and save stuff here $filters = array(); foreach ( static::$filter_vars as $filter_key => $filter_var ) { if( is_array($filter_var) ) { $filter_var = $filter_var['param'] ?? $filter_key; // allows for further expansion such as special filtering etc. } elseif ( is_int($filter_key) ) { $filter_key = $filter_var; } if( isset($_REQUEST[$filter_var]) ) { $filters[$filter_key] = wp_kses_post_deep($_REQUEST[$filter_var]); } } if( !empty($filters) ) { $settings['filters'] = $filters; } else { unset( $settings['filters'] ); } // save orderby as well if( $this->orderby ) { $settings['orderby'] = $this->orderby; } else { unset( $settings['orderby'] ); } } return $settings; } public function save_default_settings( $current_settings ) { $settings = $this->get_default_settings(); if ( !empty($current_settings['context']) ) { // we're in a context, so we add it to settings as a context if ( empty($settings['contexts'] ) ) { $settings['contexts'] = array(); } $context = $current_settings['context']; $context_settings = $settings['contexts'][$context] ?? array(); $current_settings = static::merge_settings( $context_settings, $current_settings ); $current_settings = apply_filters( static::$basename . '_save_default_settings_current', $current_settings, $this ); unset( $context_settings['contexts'], $current_settings['contexts'], $current_settings['context'] ); // delete any errant or unecessary data $settings['contexts'][$context] = $current_settings; } else { $context = false; $current_settings = apply_filters( static::$basename . '_save_default_settings_current', $current_settings, $this ); $settings = static::merge_settings( $settings, $current_settings ); } $settings = apply_filters( static::$basename . '_save_default_settings_pre', $settings, $current_settings, $this, ['context' => $context] ); update_user_meta(get_current_user_id(), static::$basename . '_settings', $settings ); do_action( static::$basename . '_save_default_settings', $settings, $current_settings, $this, ['context' => $context] ); return $settings; } /** * Merges two settings array together, by either replacing the key value in $default with $settings, or merging within the arrays by replacing equivalent keys in $settings. * The only exception is the cols key which is overwritten by $settings if it exists. * * @param $default * @param $settings * * @return mixed */ public static function merge_settings ( $default, $settings ) { foreach( $settings as $key => $value ) { if ( is_array($value) ) { // anything but cols gets merged in joining values if ( $key !== 'cols' && !empty($default[$key]) && is_array($default[$key]) ) { $default[$key] = static::merge_settings( $default[$key], $value ); } else { $default[$key] = $value; } } else { $default[$key] = $value; } } return $default; } public static function get_item_limits() { $limits = [ 5, 10, 20, 50, 100 ]; return apply_filters( static::$basename . '_get_item_limits', $limits ); } /* ------------------ Static and AJAX Methods ------------------ */ public static function init(){ add_action('wp_ajax_'. static::$basename, array( static::class, 'em_ajax_table') ); add_action('wp_ajax_'. static::$basename .'_row', array( static::class, 'em_ajax_table_row') ); if ( static::$is_exportable ) { add_action( 'wp_loaded', array( static::class, 'export_init' ) ); } // determine if we're public or not, can't determine that when doing AJAX if( static::$is_frontend === null ){ static::$is_frontend = !is_admin() || !empty($_REQUEST['is_public']); } } /** * Handles AJAX Bookings admin table filtering, view changes and pagination */ public static function em_ajax_table(){ if ( !empty($_REQUEST['event_archetype']) ) { Archetypes::set_current( $_REQUEST['event_archetype'] ); } if( !empty($_REQUEST['_emnonce']) && check_admin_referer( static::$basename, '_emnonce') ){ $class_name = static::class; $EM_List_Table = new $class_name(); $EM_List_Table->display(); }else{ check_admin_referer(static::$basename); $class_name = static::class; $EM_List_Table = new $class_name(); if( !empty($_REQUEST['table_id']) ) { // so modals work linked to the ID $EM_List_Table->uid = $EM_List_Table->id . '-' . absint($_REQUEST['table_id']); } $EM_List_Table->output_table(); } exit(); } public static function em_ajax_table_row(){ if( !empty($_REQUEST['_emnonce']) && check_admin_referer(static::$basename, '_emnonce') && !empty($_REQUEST['row_action']) ){ static::em_ajax_table_row_action( $_REQUEST['row_action'] ); } exit(); } public static function em_ajax_table_row_action( $requested_action ){} /* ------------------ Bulk and Row action links/options ------------------ */ public function get_action_url( $item ) { return esc_url_raw ( add_query_arg( array( 'action' => static::$basename . '_row', 'row_id' => $item->ID ) ) ); } public function get_action_data() { if( !empty($this->action_data) ) return $this->action_data; $actions = $this->get_action_data_items(); // add confirmation messages context foreach( $actions as $action => $action_data ) { // get link confirmation messages if( isset($action_data['actions']) ) { // sub-array of actions foreach( $action_data['actions'] as $the_action => $the_action_data ) { // get bulk and link confirmation messages - confirmation messages according to context on each action $actions[ $action ]['actions'][ $the_action ]['confirm'] = $this->get_action_message( $the_action, $the_action_data['data']['context'] ); $actions[ $action ]['actions'][ $the_action ]['confirm-bulk'] = $this->get_bulk_action_message( $the_action, $the_action_data['data']['context'] ); } } else { $actions[ $action ]['confirm'] = $this->get_action_message( $action, $action_data['data']['context'] ); $actions[ $action ]['confirm-bulk'] = $this->get_bulk_action_message( $action, $action_data['data']['context'] ); } } $this->action_data = apply_filters( static::$basename . '_get_action_data', $actions, $this ); return $this->action_data; } public function get_action_data_items() { $data_template = [ 'action' => static::$basename . '_row', 'context' => $this->context ]; $actions = [ 'delete' => [ 'label' => __('Delete','events-manager'), 'data' => array_merge( $data_template, [ 'row_action' => 'delete' ] ), ], ]; return apply_filters( 'em_list_table_get_action_data_items', $actions, $this, ['data_template' => $data_template] ); } /** * Gets action data and appends extra data to the 'data' of each action. * @param array $extra_data * * @return array */ public function get_action_data_extra( $extra_data ) { $actions = $this->get_action_data(); foreach( $actions as $action => $action_data ) { if ( isset($action_data['actions']) ) { // go one level deeper foreach( $action_data['actions'] as $the_action => $the_action_data ) { $actions[$action]['actions'][$the_action]['data'] = array_merge( $the_action_data['data'], $extra_data ); } } else { $actions[$action]['data'] = array_merge( $actions[$action]['data'], $extra_data ); } } return apply_filters( 'em_list_table_get_action_data_extra', $actions, $this, ['extra_data' => $extra_data] );; } public function get_action_message( $action, $context ) { if( $action === 'delete' ) { $message = __("Are you sure you want to delete? This action cannot be undone.",'events-manager'); } return apply_filters( 'em_list_table_get_action_message', $message ?? false, $this ); } public function get_bulk_action_message( $action, $context ) { if ( $action === 'delete' ) { $message = __( "Are you sure you want to delete this item? This action cannot be undone.", 'events-manager' ); } return apply_filters( 'em_list_table_get_bulk_action_message', $message ?? '', $this ); } /** * Generate a list of booking action links * * @param $item * * @return array */ public function get_action_links( $item ) { $action_data = $this->get_action_data(); $url = $this->get_action_url( $item ); $booking_actions = $this->build_action_link( $action_data, $url ); return apply_filters(static::$basename . '_cols_col_action', $booking_actions, $item, $this); } public function build_action_link ( $data, $url = '' ) { $action_links = array(); foreach( $data as $action => $action_data ) { if( !empty($action_data['actions']) ) { $action_links[$action] = [ 'actions' => $this->build_action_link( $action_data['actions'], $url ), 'label' => $action['label'] ?? '', ]; } else { $data = array_merge( $action_data['data'], ['nonce' => wp_create_nonce( $action_data['data']['action'] ?? $action )] ); $attributes = array(); foreach( $data as $att => $att_val ) { $attributes[] = 'data-' . esc_html($att) . '="' . esc_attr($att_val) . '"'; } $action_links[$action] = '<a class="em-list-table-row-action" href="'.em_add_get_params($url, $action_data['data']).'"'. implode(' ', $attributes) .'>'.$action_data['label'].'</a>'; } } return $action_links; } public function get_bulk_actions() { $bulk_actions = $this->get_action_data(); return apply_filters(static::$basename . '_get_bulk_actions', $bulk_actions, $this); } public function output_action_links ( $action_links ) { ob_start(); foreach( $action_links as $action => $action_link ) { if( isset($action_link['actions']) ) { echo '<section class="'. esc_attr($action) .'">'; echo $this->output_action_links( $action_link['actions'] ); echo '</section>'; } else { echo $action_link; } } return ob_get_clean(); } /* ------------------ Table Data ------------------ */ public static function sanitize_spreadsheet_cell( $cell ){ if ( !empty($cell) || is_numeric($cell) ) { if ( is_array($cell) ) { $cell = implode( ',', $cell ); } if ( !is_array( $cell ) ) { $cell = html_entity_decode( $cell ); //remove things like & which may have been saved to the DB directly return preg_replace( '/^([;=@\+\-])/', "'$1", $cell ); } } return ''; } /** * Prepare the items for the table to process * * @return Void */ public function prepare_items(){ $columns = $this->get_columns(); // wrap column data up in case we have html in them $hidden = $this->get_hidden_columns(); $sortable = $this->get_sortable_columns(); $this->per_page = $this->get_items_per_page( $this->per_page_var, $this->per_page ); $this->items = $this->table_data(); $this->set_pagination_args( array( 'total_items' => $this->total_items, 'per_page' => $this->per_page, 'total_pages' => ceil($this->total_items / $this->per_page), ) ); $this->_column_headers = array( $columns, $hidden, $sortable ); } protected function get_items_per_page( $option, $default_value = 20 ) { return $this->limit; } /* ------------------ Table Output ------------------ */ /** * Displays the table * @return void */ public function display(){ do_action(static::$basename . '_header', $this); //won't be overwritten by JS $this->prepare_items(); $uid = esc_attr($this->uid); // setup additional classes $extra_classes = $this->display_classes(); $atts = array(); foreach( $this->display_attributes() as $att => $att_val ) { $atts[] = esc_html($att) . '="' . esc_attr($att_val) . '"'; } // get confirmation messages for actions and put it in the form element, to keep the wrapper a little less cluttered $actions = $this->get_action_data(); $action_messages = []; foreach ( $actions as $action => $action_data ) { if( isset($action_data['actions']) ) { foreach ( $action_data['actions'] as $the_action => $the_action_data ) { if ( !empty($the_action_data['confirm']) ) { $action_messages[ $the_action ] = esc_attr( $the_action_data['confirm'] ); } } } else { if ( !empty($action_data['confirm']) ) { $action_messages[$action] = esc_attr($action_data['confirm']); } } } $action_messages = apply_filters( static::$basename . '_action_messages', $action_messages, $this ); ?> <div class="em-list-table <?php em_template_classes(static::$template_component_name); echo ' ' . implode(' ', $extra_classes); ?>" id="<?php echo $uid; ?>" data-basename="<?php echo static::$basename; ?>" <?php echo implode(' ', $atts); ?>> <form class='<?php echo static::$form_class ?> em-list-table-form' method='post' action='' id="<?php echo $uid; ?>-form" data-action-messages='<?php echo esc_attr(json_encode($action_messages)); ?>'> <input type="hidden" name="is_public" value="<?php echo ( static::$is_frontend ) ? 1:0; ?>"> <input type="hidden" name="pno" value='<?php echo esc_attr($this->page); ?>'> <input type="hidden" name="order" value='<?php echo esc_attr($this->order); ?>' data-persist> <input type="hidden" name="orderby" value='<?php echo esc_attr($this->orderby); ?>' data-persist> <input type="hidden" name="action" value="<?php echo static::$basename; ?>"> <input type="hidden" name="cols" value="<?php if ( !empty($_REQUEST['cols']) ) echo esc_attr(implode(',', $this->cols)); ?>"> <input type="hidden" name="limit" value="<?php echo esc_attr($this->limit); ?>"> <input type="hidden" name="_emnonce" value="<?php echo wp_create_nonce(static::$basename); ?>"> <input type="hidden" name="save" value="0"> <input type="hidden" name="save_filters" value="0"> <input type="hidden" name="table_id" value="<?php echo esc_attr( str_replace($this->id . '-', '', $uid)); ?>"> <input type="hidden" name="event_archetype" value="<?php echo esc_attr(Archetypes::get_current()); ?>"> <?php $this->display_hidden_input(); parent::display(); ?> </form> <?php $this->output_overlays(); ?> </div> <?php do_action(static::$basename . '_footer',$this); //won't be overwritten by JS } public function display_classes() { $extra_classes = array(); $extra_classes[] = $this->total_items && $this->checkbox_id ? 'has-checkboxes':'no-checkboxes'; $extra_classes[] = $this->total_items && static::$is_exportable ? 'has-export':'no-export'; $extra_classes[] = static::$has_filters ? 'has-filter':'no-filter'; $extra_classes[] = static::$is_frontend ? 'frontend':'backend'; return $extra_classes; } public function display_attributes() { return array(); } /** * Override to output table-specific hidden columns * @return void */ public function display_hidden_input() {} /** * Copies and overrides the parent WP_List_Table method. * * Adds a wrapper to the top/bottom of the actual table of values, and wraps the tablenav actions in another div.alignleft.actions * * This will allow for better responsive reactions with regards to pagination. * * @param $which * * @return void */ protected function display_tablenav( $which ) { if ( 'top' === $which ) { wp_nonce_field( 'bulk-' . $this->_args['plural'] ); } else { echo '</div>'; if ( !self::$show_bottom_tablenav ) return; } ?> <div class="tablenav <?php echo esc_attr( $which ); ?>"> <?php if ( 'top' === $which || self::$show_bottom_tablenav_actions ) : ?> <?php if ( ( 'top' === $which || self::$show_bottom_tablenav_bulkactions ) && $this->has_items() ) { ?> <div class="alignleft actions bulkactions"> <?php $this->bulk_actions( $which ); ?> </div> <?php } if ( ( 'top' === $which || self::$show_bottom_tablenav_extra_tablenav ) ) { $this->extra_tablenav( $which ); } ?> <?php endif; ?> <?php if( ( 'top' === $which || self::$show_bottom_tablenav_pagination ) ) { $this->pagination( $which ); } ?> </div> <?php if ( 'top' === $which ) { echo '<div class="table-wrap">'; } } function output_overlays(){ $this->output_overlay_settings(); if( static::$is_exportable ) { $this->output_overlay_export(); } } function output_overlay_export(){ // join all fields into one set of cols, with general taking precendence over events, then tickets then attendees $cols_template = $this->get_cols_template(); $grouped_fields = array(); foreach( $this->cols_template_groups as $group_data ){ $grouped_fields = array_merge($grouped_fields, $group_data['fields']); } $ungrouped_fields = array_diff(array_keys($cols_template), $grouped_fields); $uid = esc_attr($this->uid); $id = esc_attr($this->id); ?> <div class="em pixelbones em-modal em-list-table-export em-list-table-modal" id="<?php echo $uid; ?>-export-modal"> <form id="<?php echo $uid; ?>-export-form" class="em-list-table-form em-list-table-export-form" action="" method="post" rel="#<?php echo $uid . '-form'; ?>"> <div class="em-modal-popup"> <header> <a class="em-close-modal"></a><!-- close modal --> <div class="em-modal-title"><?php esc_attr_e('Bookings Table Settings','events-manager'); ?></div> </header> <div class="em-modal-content input"> <p><?php esc_html_e('Select the options below and export all the records you have currently filtered (all pages) into a spreadsheet format.','events-manager') ?></p> <?php do_action( static::$basename . '_export_options', $this); ?> <?php static::output_overlay_columns_selection( array( 'text' => array( 'show' => array( 'title' => __('Columns to export','events-manager'), ), 'choose' => array( 'title' => __('Exporter','events-manager'), ) ), )); ?> </div> <footer class="em-submit-section input"> <div> <div class="<?php echo $id; ?>-filters em-list-table-filters" style="display:none; visibility:hidden;"><!-- houses filter data as modified for export --></div> <input type="hidden" name="no_save" value='1'> <input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( static::$export_action ); ?>"> <input type="hidden" name="_emnonce" value="<?php echo wp_create_nonce( static::$export_action ); ?>"> <input type="hidden" name="action" value="<?php echo esc_attr(static::$export_action); ?>"> <button type="submit" class="button button-primary"><?php esc_html_e('Export', 'events-manager'); ?></button> </div> </footer> </div> </form> </div> <br class="clear"> <?php } function output_overlay_settings(){ $uid = esc_attr($this->uid); $id = esc_attr($this->id); ?> <div class="em pixelbones em-modal em-list-table-settings em-list-table-modal" id="<?php echo $uid; ?>-settings-modal"> <form id="<?php echo $uid; ?>-settings-form" class="em-list-table-form em-list-table-settings-form" action="" method="post" rel="#<?php echo $uid . '-form'; ?>"> <div class="em-modal-popup"> <header> <a class="em-close-modal"></a><!-- close modal --> <div class="em-modal-title"><?php esc_attr_e('Bookings Table Settings','events-manager'); ?></div> </header> <div class="em-modal-content input"> <p><?php _e('Modify what information is displayed in this booking table.','events-manager') ?></p> <div class="<?php echo $uid; ?>-rows-setting em-list-table-setting"> <label for="<?php echo $uid; ?>-rows-setting"><strong><?php esc_html_e('Results per Page', 'events-manager'); ?></strong></label> <select name="limit" class="<?php echo $id; ?>-filter" id="<?php echo $uid; ?>-rows-setting"> <?php foreach ( static::get_item_limits() as $limit ) : ?> <option value="<?php echo esc_attr($limit) ?>" <?php selected($limit, $this->limit); ?>><?php echo esc_html(sprintf(__('%s Rows','events-manager'),$limit)); ?></option> <?php endforeach; ?> </select> </div> <?php do_action( static::$basename . '_settings_options', $this); ?> <?php static::output_overlay_columns_selection(); ?> </div> <footer class="em-submit-section input"> <div> <button type="submit" class="button button-primary"><?php esc_html_e('Save Settings', 'events-manager'); ?></button> </div> <div class="em-list-table-settings-remember"> <label> <?php esc_html_e('Remember these settings', 'events-manager'); ?> <?php $this->output_overlay_settings_remember_tooltip(); ?> <input type="checkbox" name="save" value="1" checked="checked" data-setting> </label> <label> <?php esc_html_e('Remember filtering options', 'events-manager'); ?> <?php $this->output_overlay_settings_remember_filters_tooltip(); ?> <input type="checkbox" name="save_filters" value="1" data-setting> </label> </div> </footer> </div> </form> </div> <?php } public function output_overlay_settings_remember_tooltip() {} public function output_overlay_settings_remember_filters_tooltip() { ?> <span class="em-icon em-icon-info s-15 em-tooltip" aria-label="<?php esc_html_e('Your search filter settings will be selected automatically.', 'events-manager'); ?>" data-tippy-maxWidth="250px"></span> <?php } public function output_overlay_columns_selection ( $options = array() ) { $options = array_replace_recursive( array( 'text' => array( 'show' => array( 'title' => __('Columns to show','events-manager'), 'description' => __('Remove or reorder columns below.','events-manager') ), 'choose' => array( 'title' => __('Columns to choose','events-manager'), 'description' => __('Add to your table from the columns below.','events-manager') ), ), ), $options); // join all fields into one set of cols, with general taking precendence over events, then tickets then attendees $cols_template = $this->get_cols_template(); $grouped_field_keys = array(); $grouped_fields = array(); // get the keys of all grouped data, and also get the labels into grouped array so we can order alphabetically now whilst we're at it foreach( $this->cols_template_groups as $group_key => $group_data ){ $grouped_field_keys = array_merge($grouped_field_keys, $group_data['fields']); $grouped_fields[$group_key] = []; foreach( $group_data['fields'] as $col ) { $grouped_fields[$group_key][$col] = $cols_template[$col]['column_header'] ?? $cols_template[$col]; } uasort( $grouped_fields[$group_key], function ( $a, $b ){ $col_label_a = $a['label'] ?? $a; $col_label_b = $b['label'] ?? $b; return strnatcasecmp($col_label_a, $col_label_b); }); } // get ungrouped keys and sort them alphabetically too $ungrouped_fields = array_diff_key($cols_template, array_combine($grouped_field_keys, $grouped_field_keys)); uasort( $ungrouped_fields, function ( $a, $b ){ $col_label_a = $a['label'] ?? $a; $col_label_b = $b['label'] ?? $b; return strnatcasecmp($col_label_a, $col_label_b); }); ?> <div class="em-list-table-cols"> <div class="em-list-table-cols-selected"> <p> <strong><?php echo $options['text']['show']['title']; ?></strong><br> <?php echo $options['text']['show']['description'] ?> </p> <div class="em-list-table-cols-sortable"> <?php foreach( $this->cols as $col_key ): ?> <div class="item" data-value="<?php echo esc_attr($col_key); ?>"> <span> <?php $col_header = $this->cols_template[$col_key]['column_header'] ?? $this->cols_template[$col_key]; echo esc_html($col_header); ?> </span> <a href="#" class="remove" tabindex="-1" title="Remove">×</a> <input type="hidden" name="cols[<?php echo esc_attr($col_key); ?>]" value="1" class="em-bookings-col-item"> </div> <?php endforeach; ?> </div> </div> <div class="em-list-table-cols-select"> <p> <strong><?php echo $options['text']['choose']['title']; ?></strong><br> <?php echo $options['text']['choose']['description'] ?> </p> <select class="em-list-table-cols-inactive em-selectize always-open checkboxes" multiple> <?php foreach( $grouped_fields as $group_key => $cols ): ?> <optgroup label="<?php echo esc_attr($this->cols_template_groups[$group_key]['label']); ?>" data-data='{"type":"<?php echo esc_attr($group_key) ?>"}'> <?php foreach( $cols as $col_key => $col_data ): ?> <?php $col_header = $col_data['column_header'] ?? $col_data; $col_label = $col_data['label'] ?? $col_data; ?> <option value="<?php echo esc_attr($col_key); ?>" data-data='{"type":"<?php echo esc_attr($col_label) ?>", "header":"<?php echo esc_attr($col_header); ?>"}' <?php if( in_array($col_key, $this->cols) ) echo 'selected'; ?>> <?php echo esc_html($col_label); ?> </option> <?php endforeach; ?> </optgroup> <?php endforeach; ?> <optgroup label="<?php esc_html_e('Other', 'events-manager'); ?>" data-data='{"type":"other"}'> <?php foreach( $ungrouped_fields as $col_key => $col_data ): ?> <?php $col_header = $col_data['column_header'] ?? $col_data; $col_label = $col_data['label'] ?? $col_data; ?> <option value="<?php echo esc_attr($col_key); ?>" data-data='{"type":"other", "header":"<?php echo esc_attr($col_header); ?>"}' <?php if( in_array($col_key, $this->cols) ) echo 'selected'; ?>> <?php echo esc_html($col_label); ?> </option> <?php endforeach; ?> </optgroup> </select> </div> </div> <?php } /** * Overrides default by adding and then unsetting $_GET['orderby'] to show the inisial ordering mechanism * * @param bool $with_id Whether to set the ID attribute or not */ public function print_column_headers( $with_id = true ) { if( $this->orderby && !isset($_GET['orderby']) ) { // set and unset $_GET to have a default ordering $_GET['orderby'] = $this->orderby; parent::print_column_headers( $with_id ); unset($_GET['orderby']); } else { // just output witout modifying $_GET parent::print_column_headers( $with_id ); } } /** * Define which columns are hidden * * @return array */ public function get_hidden_columns(){ return array(); } /** * Get the table data, can get overrwritten for more customization. * * @return array * @uses $this->get_items() */ protected function table_data(){ // Do the search and return result return $this->get_items(); } /** * Performs a search based on requested table data and populates the $this->items array with the results, returning the same. * * @return array */ protected function get_items () { return array(); } /** * Gets header columns for this list table. * @deprecated @param bool $csv * * @return mixed|null */ function get_headers( $csv = false ) { // temp switch $this->format so we're consistent but adhere to the deprecated $csv parameter $old_format = $this->format; $this->format = $csv ? 'csv' : $this->format; $headers = array(); foreach ( $this->cols as $col ) { $spreadsheet_format = in_array( $this->format, ['csv', 'xls', 'xlsx'] ); if ( $col == 'actions' ) { if ( !$spreadsheet_format ) { $headers[ $col ] = ' '; } } elseif ( array_key_exists( $col, $this->cols_template ) ) { if( is_array($this->cols_template[ $col ]) ) { $v = $this->cols_template[ $col ]['column_header'] ?? $this->cols_template[ $col ]['label']; } else { $v = $this->cols_template[ $col ]; } //csv/excel escaping if ( $spreadsheet_format ) { $v = static::sanitize_spreadsheet_cell( $v ); $headers[ $col ] = $v; } else { $headers[ $col ] = $v; } } } $headers = apply_filters( 'em_list_table_get_headers', $headers, $this ); $headers = apply_filters( static::$basename . '_get_headers', $headers, $this ); if ( $csv ) { // undo backpat for $csv $this->format = $old_format; } return $headers; } /** * Gets a single row in HTML format for output to table HTML format. * @param \EM_Object $item * * @return void */ public function single_row( $item ) { echo '<tr data-id="'. esc_attr($item->id) .'">'; $this->single_row_columns( $item ); echo '</tr>'; } /** * Simlar to single_row(), but returns an array of data, excluding checkboxes and actions, which can be used for export. * * IMPORTANT - this function will return an array of arrays, where each array is a row of data. This allows for splitting of data into multiple rows for export. * * @param $EM_Object * @param $format * * @return array[] */ public function get_row( $EM_Object ) { $cols = array(); foreach( $this->cols as $col ){ //TODO fix urls so this works in all pages in front as well //add to cols $cols[$col] = $this->default_column_data( $EM_Object, $col ); } // the specific basename filter will provide a single-dimensional array, you can return a multi-dimensional array at this point $cols = apply_filters( static::$basename. '_get_row', $cols, $EM_Object, $this ); if( !is_array( current($cols) ) ) { $cols = array($cols); } return apply_filters( 'em_list_table_get_row', $cols, $EM_Object, $this ); } function get_cols_template(){ return $this->cols_template; } /** * Override the parent columns method. Defines the columns to use in your listing table * * @return array */ public function get_columns() { $columns = array(); if( $this->checkbox_id ) { $columns['cb'] = '<input type="checkbox" />'; } return $columns + $this->get_headers(); } /** * Define what data to show on each column of the table * * @param \EM_Object $item Data, usually an EM_Object for this class. Override this method to use your own data. * @param string $column_name Current column name * * @return string */ function column_default( $item, $column_name ){ $val = ''; if( in_array($column_name, $this->cols) ){ // get the column values $primary_column = array_values($this->cols)[0]; // changed from reset() to avoid potential issues resetting nested foreaches if( $primary_column === $column_name && $this->format == 'html' ){ $val = $this->primary_column_meta( $item, $column_name ); $val .= '<div class="primary-column-content" data-colname="'. $this->_column_headers[0][$column_name] .'">'. $this->default_column_data( $item, $column_name ) . '</div>'; if( static::$show_responsive_meta ) { $val .= '<div class="em-list-table-row-responsive-meta">' . $this->primary_column_responsive_meta( $item, $column_name ) . '</div>'; } } else { $val = $this->default_column_data( $item, $column_name ); } } return $val; } public function primary_column_meta( $item, $column_name ) { $content = ''; // ajax status icons if( !empty( $item->feedback_message) ){ $css_icon = empty( $item->errors ) ? 'updated' : 'cross-circle'; $content = '<span href="#" class="em-icon em-icon-'.$css_icon.' em-tooltip" aria-label="'. $item->feedback_message .'"></span> '; } return $content; } public function primary_column_responsive_meta( $item, $column_name ) { return ''; } /** * Gets data for columns that don't have a specific method defined. * * This method MUST be overwritten by child classes and ALSO must run the return value through the default_column_sanitize_data() method. * @param $item * @param $col * * @return string */ public function default_column_data( $item, $col ){ $val = $this->get_default_column_data( $item, $col ); // interept data before it is sanitized $val = apply_filters( 'em_list_table_rows_col_'.$col, $val, $item, $this ); $val = apply_filters( static::$basename.'_rows_col_'.$col, $val, $item, $this, $this->format, $this); // IGNORE LAST $this in your filters (bottom one too), typo and we don't want to remove it just in case it breaks sites implementing it by mistake $val = apply_filters( static::$basename.'_rows_col', $val, $col, $item, $this, $this->format, $this); // use the above filter instead for better performance // sanitize data, intercepting data should set and unset (0) itself each time accordingly from the $this->allow_html array return $this->default_column_sanitize_data( $val, $col, $item ); } public function get_default_column_data( $item, $col ) { _doing_it_wrong( __METHOD__, 'You must overwrite this method in your child class.', '5.9.0' ); return ''; } public function default_column_sanitize_data( $val, $col, $item, $allow_html = false ) { $format = $this->format; //escape all HTML if destination is HTML or not defined if( empty($allow_html) && ($format == 'html' || empty($format)) && empty(static::$cols_allowed_html[$col]) ){ $val = esc_html($val); } //csv/excel escaping if( in_array( $format, ['csv', 'xls', 'xlsx'] ) ){ $val = self::sanitize_spreadsheet_cell($val); } return $val; } /** * Bulk Edit Checkbox * @param array $item * @return string */ function column_cb( $item ) { if( is_object($item) ){ $id = $item->{$this->checkbox_id}; }else{ $id = $item[$this->checkbox_id]; } return sprintf('<input type="checkbox" name="column_id[]" value="%s" />', $id); } public function bulk_actions ( $which = '' ) { $uid = esc_attr($this->uid); $id = esc_attr($this->id); ?> <div class="alignleft actions <?php echo $id; ?>-settings em-list-table-triggers"> <a href="#" class="<?php echo $id; ?>-export <?php echo $id; ?>-trigger em-list-table-trigger em-list-table-export-trigger em-icon em-icon-download em-tooltip" id="<?php echo $id; ?>-export-trigger" rel="#<?php echo $uid; ?>-export-modal" aria-label="<?php _e('Export these bookings.','events-manager'); ?>"></a> <a href="#" class="<?php echo $id; ?>-settings <?php echo $id; ?>-trigger em-list-table-trigger em-list-table-settings-trigger em-icon em-icon-settings em-tooltip" id="<?php echo $id; ?>-settings-trigger" rel="#<?php echo $uid; ?>-settings-modal" aria-label="<?php _e('Settings','events-manager'); ?>"></a> </div> <?php $this->extra_tablenav_trigger( $which ); ?> <?php if( $this->checkbox_id ) { $this->bulk_actions_input( $which ); } } public function bulk_actions_input( $which = '' ) { $id = esc_attr($this->id); $uid = esc_attr($this->uid); ?> <div class="alignleft actions bulkactions-input"> <label for="<?php echo $uid; ?>-bulk-action-selector-top" class="screen-reader-text"><?php esc_html_e('Select bulk action'); ?></label> <select class="bulk-action-selector em-list-table-bulk-action" id="<?php echo $uid; ?>-bulk-action-selector<?php if( $which ) echo '-'.$which; ?>"> <option value="-1"><?php esc_html_e('Bulk actions', 'events-manager'); ?></option> <?php foreach( $this->get_bulk_actions() as $action => $action_data ): ?> <?php if( !empty($action_data['actions']) ): ?> <optgroup label="<?php echo esc_attr($action_data['label'] ?? '---'); ?>"> <?php foreach( $action_data['actions'] as $sub_action => $sub_action_data ): ?> <?php $data = array(); $sub_action_data['data']['confirm'] = $sub_action_data['confirm-bulk'] ?? ''; foreach( $sub_action_data['data'] as $key => $value ){ if( $value ) $data[] = 'data-' .esc_attr($key) . '="' . esc_attr($value) . '"'; } ?> <option value="<?php echo esc_attr($sub_action); ?>" <?php echo implode(' ', $data); ?>><?php echo esc_html($sub_action_data['label']); ?></option> <?php endforeach; ?> </optgroup> <?php else: ?> <?php $data = array(); $action_data['data']['confirm'] = $action_data['confirm-bulk'] ?? ''; // add bulk message foreach( $action_data['data'] as $key => $value ){ if( $value ) $data[] = 'data-' . esc_attr($key) . '="' . esc_attr($value) . '"'; } ?> <option value="<?php echo esc_attr($action); ?>" <?php echo implode(' ', $data); ?>><?php echo esc_html($action_data['label']); ?></option> <?php endif; ?> <?php endforeach; ?> </select> <button class="button <?php echo $id; ?>-bulk-action em-list-table-bulk-action"><?php esc_html_e('Apply'); ?></button> </div> <?php } public function extra_tablenav_trigger ( $which = '' ) { $tooltip_text = static::$show_filters ? esc_html__('Hide Search', 'events-manager') : esc_html__('Show Search', 'events-manager'); $id = esc_attr($this->id); ?> <div class="alignleft actions <?php echo $id; ?>-settings"> <button class="button filters-trigger <?php echo esc_attr($this->id); ?>-filters-trigger em-tooltip <?php if ( !static::$show_filters ) echo 'hidden'; ?>" aria-label="<?php echo $tooltip_text; ?>" data-label-show="<?php esc_html_e('Show Search', 'events-manager'); ?>" data-label-hide="<?php esc_html_e('Hide Search', 'events-manager'); ?>" ><?php esc_html_e('Show/Hide Search'); ?></button> <button class="button small-expand-trigger <?php echo esc_attr($this->id); ?>-small-expand-trigger"><?php esc_html_e('Expand Columns'); ?></button> </div> <?php } /* START EXPORTING */ /** * Invoked on wp_loaded, allowing for exports via AJAX or directly on any page, checks $_REQUEST vars and verifies nonce, triggers an export if request is valid. * @return void */ public static function export_init() { if ( !empty($_REQUEST['action']) && !empty($_REQUEST['_emnonce']) && $_REQUEST['action'] === static::$export_action ) { if( wp_verify_nonce($_REQUEST['_emnonce'], static::$export_action) ) { static::$exporting = true; static::export_csv(); } else { wp_die( 'Cannot export, security nonce verification failed.' ); } } } /** * Exports and outputs a CSV downloadable file to requester. * @return void */ public static function export_csv () { do_action( 'export_csv_pre', static::class ); // open the file handle $handle = fopen("php://output", "w"); // setup delimiter $delimiter = !defined('EM_CSV_DELIMITER') ? static::$export_delimiter : EM_CSV_DELIMITER; static::$export_delimiter = apply_filters('em_csv_delimiter', $delimiter); // set it static, we're exiting after export finishes // send headers static::export_csv_http_headers(); // output header title static::export_csv_header_title( $handle ); // prep table headers $_REQUEST cols and limit for use when setting up list table if( !empty($_REQUEST['cols']) && is_array($_REQUEST['cols']) ){ $cols = array(); foreach($_REQUEST['cols'] as $col => $active){ if( $active ){ $cols[] = $col; } } $_REQUEST['cols'] = $cols; } $_REQUEST['limit'] = 0; // setup list table $EM_List_Table = static::export_setup_list_table( $handle ); // output table headers static::export_csv_table_headers( $EM_List_Table, $handle ); // output table rows static::export_csv_table_rows( $EM_List_Table, $handle ); // close and exit fclose($handle); exit(); } public static function export_csv_http_headers() { //generate bookings export according to search request if( !empty($_REQUEST['event_id']) ){ $EM_Event = em_get_event( absint($_REQUEST['event_id']) ); } header("Content-Type: application/octet-stream; charset=utf-8"); $file_name = $EM_Event->event_slug ?? get_bloginfo(); header("Content-Disposition: Attachment; filename=".sanitize_title($file_name)."-export.csv"); do_action('em_csv_header_output'); echo "\xEF\xBB\xBF"; // UTF-8 for MS Excel (a little hacky... but does the job) } public static function export_csv_header_title( $handle ) { // get Event ID if( !empty($_REQUEST['event_id']) ){ $EM_Event = em_get_event( absint($_REQUEST['event_id']) ); } // csv headers if ( !defined('EM_CSV_DISABLE_HEADERS') || !EM_CSV_DISABLE_HEADERS ) { if( !empty($_REQUEST['event_id']) ) { fputcsv($handle, array( __('Event','events-manager') . ' : ' . $EM_Event->event_name ), static::$export_delimiter); if( $EM_Event->location_id > 0 ) { fputcsv($handle, array( __('Where','events-manager') . ' - ' . $EM_Event->get_location()->location_name ), static::$export_delimiter); } fputcsv($handle, array( __('When','events-manager') . ' : ' . $EM_Event->output('#_EVENTDATES - #_EVENTTIMES') ), static::$export_delimiter); } $EM_DateTime = new \EM_DateTime(current_time('timestamp')); fputcsv($handle, array( sprintf(__('Exported on %s','events-manager'), $EM_DateTime->format('D d M Y h:i')) ), static::$export_delimiter); fputcsv($handle, array(), static::$export_delimiter); } } public static function export_setup_list_table( $handle, $format = 'csv' ) { $class = static::class; $EM_List_Table = new $class(); $EM_List_Table->format = $format; $EM_List_Table->limit = 150; //if you're having server memory issues, try messing with this number return $EM_List_Table; } public static function export_csv_table_headers( $EM_List_Table, $handle ) { fputcsv($handle, $EM_List_Table->get_headers(true), static::$export_delimiter); } /** * @param resource $handle * @param List_Table $EM_List_Table * * @return void */ public static function export_csv_table_rows( $EM_List_Table, $handle ) { $items = $EM_List_Table->table_data(); while( !empty($items) ){ foreach( $items as $item ) { static::export_csv_table_row( $item, $EM_List_Table, $handle ); } //reiterate loop $EM_List_Table->offset += $EM_List_Table->limit; $items = $EM_List_Table->table_data(); } } /** * Exports a single or multiple rows based on what is provided from $this->get_row( $item ); * @param \EM_Object $item * @param List_Table $EM_List_Table * @param resource $handle * * @return void */ public static function export_csv_table_row( $item, $EM_List_Table, $handle ) { $rows = $EM_List_Table->get_row( $item ); foreach( $rows as $row ) { fputcsv( $handle, $row, static::$export_delimiter ); } } /* END EXPORTING */ } } namespace { function em_list_table_create_funcitions(){ // handle convert_to_screen lacking in places if( !function_exists('convert_to_screen')){ function convert_to_screen( $hook_name ) { return new \EM\WP_Screen; } } if( !function_exists('get_column_headers') ){ function get_column_headers( $screen ) { return array(); } } // slightly risky situation... if WP_Screen wasn't loaded but convert_to_screen was defined, we need to make sure WP_Screen exists and hope another plugin isn't adding it later in the page if( !class_exists('WP_Screen') ) { class WP_Screen extends \EM\WP_Screen {} } } include('em-wp-screen.php'); }
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка