ultimate_helper.php 0000644 00000021767 15021222626 0010453 0 ustar 00 $value ) {
if ( ! empty( $value ) ) {
if ( stripos( $value, '^' ) !== false ) {
$tmvav_array = explode( '^', $value );
if ( is_array( $tmvav_array ) && ! empty( $tmvav_array ) ) {
if ( ! empty( $tmvav_array ) ) {
if ( isset( $tmvav_array[0] ) ) {
$mainarr[ $tmvav_array[0] ] = ( isset( $tmvav_array[1] ) ) ? $tmvav_array[1] : '';
}
}
}
} else {
$mainarr['id'] = $temp_id;
$mainarr['url'] = $temp_url;
}
}
}
}
if ( '' != $data ) {
switch ( $data ) {
case 'url': // First - Priority for ID.
if ( ! empty( $mainarr['id'] ) && 'null' != $mainarr['id'] ) {
$image_url = '';
// Get image URL, If input is number - e.g. 100x48 / 140x40 / 350x53.
if ( 1 === preg_match( '/^\d/', $size ) ) {
$size = explode( 'x', $size );
// resize image using vc helper function - wpb_resize.
$img = wpb_resize( $mainarr['id'], null, $size[0], $size[1], true );
if ( $img ) {
$image_url = $img['url'];
}
} else {
// Get image URL, If input is string - [thumbnail, medium, large, full].
$hasimage = wp_get_attachment_image_src( $mainarr['id'], $size ); // returns an array.
$image_url = isset( $hasimage[0] ) ? $hasimage[0] : '';
}
if ( isset( $image_url ) && ! empty( $image_url ) ) {
$final = $image_url;
} else {
// Second - Priority for URL - get {image from url}.
if ( isset( $mainarr['url'] ) ) {
$final = ult_get_url( $mainarr['url'] );
}
}
} else {
// Second - Priority for URL - get {image from url}.
if ( isset( $mainarr['url'] ) ) {
$final = ult_get_url( $mainarr['url'] );
}
}
break;
case 'title':
$final = isset( $mainarr['title'] ) ? $mainarr['title'] : get_post_meta( $mainarr['id'], '_wp_attachment_image_title', true );
break;
case 'caption':
$final = isset( $mainarr['caption'] ) ? $mainarr['caption'] : get_post_meta( $mainarr['id'], '_wp_attachment_image_caption', true );
break;
case 'alt':
$final = isset( $mainarr['alt'] ) ? $mainarr['alt'] : get_post_meta( $mainarr['id'], '_wp_attachment_image_alt', true );
break;
case 'description':
$final = isset( $mainarr['description'] ) ? $mainarr['description'] : get_post_meta( $mainarr['id'], '_wp_attachment_image_description', true );
break;
case 'json':
$final = wp_json_encode( $mainarr );
break;
case 'sizes':
$img_size = getimagesqueresize( $img_id, $img_size );
$img = wpb_getImageBySize(
array(
'attach_id' => $img_id,
'thumb_size' => $img_size,
'class' => 'vc_single_image-img',
)
);
$final = $img;
break;
case 'array':
default:
$final = $mainarr;
break;
}
}
}
return $final;
}
add_filter( 'ult_get_img_single', 'ult_img_single_init', 10, 3 );
}
if ( ! function_exists( 'ult_get_url' ) ) {
/**
* Ult_get_url.
*
* @param string $img Img.
*/
function ult_get_url( $img ) {
if ( isset( $img ) && ! empty( $img ) ) {
return $img;
}
}
}
// USE THIS CODE TO SUPPORT CUSTOM SIZE OPTION.
if ( ! function_exists( 'getimagesqueresize' ) ) {
/**
* GetImageSquereSize.
*
* @param string $img_id Image ID.
* @param string $img_size Image Size.
*/
function getimagesqueresize( $img_id, $img_size ) {
if ( preg_match_all( '/(\d+)x(\d+)/', $img_size, $sizes ) ) {
$exact_size = array(
'width' => isset( $sizes[1][0] ) ? $sizes[1][0] : '0',
'height' => isset( $sizes[2][0] ) ? $sizes[2][0] : '0',
);
} else {
$image_downsize = image_downsize( $img_id, $img_size );
$exact_size = array(
'width' => $image_downsize[1],
'height' => $image_downsize[2],
);
}
if ( isset( $exact_size['width'] ) && (int) $exact_size['width'] !== (int) $exact_size['height'] ) {
$img_size = (int) $exact_size['width'] > (int) $exact_size['height']
? $exact_size['height'] . 'x' . $exact_size['height']
: $exact_size['width'] . 'x' . $exact_size['width'];
}
return $img_size;
}
}
/* Ultimate Box Shadow */
if ( ! function_exists( 'ultimate_get_box_shadow' ) ) {
/**
* GetImageSquereSize.
*
* @param string $content Content.
* @param string $data Image Data.
*/
function ultimate_get_box_shadow( $content = null, $data = '' ) {
// e.g. horizontal:14px|vertical:20px|blur:30px|spread:40px|color:#81d742|style:inset|.
$final = '';
if ( '' != $content ) {
// Create an array.
$mainstr = explode( '|', $content );
$string = '';
$mainarr = array();
if ( ! empty( $mainstr ) && is_array( $mainstr ) ) {
foreach ( $mainstr as $key => $value ) {
if ( ! empty( $value ) ) {
$string = explode( ':', $value );
if ( is_array( $string ) ) {
if ( ! empty( $string[1] ) && 'outset' != $string[1] ) {
$mainarr[ $string[0] ] = $string[1];
}
}
}
}
}
$rm_bar = str_replace( '|', '', $content );
$rm_colon = str_replace( ':', ' ', $rm_bar );
$rmkeys = str_replace( 'horizontal', '', $rm_colon );
$rmkeys = str_replace( 'vertical', '', $rmkeys );
$rmkeys = str_replace( 'blur', '', $rmkeys );
$rmkeys = str_replace( 'spread', '', $rmkeys );
$rmkeys = str_replace( 'color', '', $rmkeys );
$rmkeys = str_replace( 'style', '', $rmkeys );
$rmkeys = str_replace( 'outset', '', $rmkeys ); // Remove outset from style - To apply {outset} box. shadow.
if ( '' != $data ) {
switch ( $data ) {
case 'data':
$final = $rmkeys;
break;
case 'array':
$final = $mainarr;
break;
case 'css':
default:
$final = 'box-shadow:' . $rmkeys . ';';
break;
}
} else {
$final = 'box-shadow:' . $rmkeys . ';';
}
}
return $final;
}
add_filter( 'ultimate_getboxshadow', 'ultimate_get_box_shadow', 10, 3 );
}
class-render.php 0000644 00000114672 15021224011 0007636 0 ustar 00
*/
protected array $table = array();
/**
* Table options that influence the output result.
*
* @since 1.0.0
* @var array
*/
protected array $render_options = array();
/**
* Rendered HTML code of the table or PHP array.
*
* @since 1.0.0
* @var string|array>
*/
protected $output;
/**
* Trigger words for colspan, rowspan, or the combination of both.
*
* @since 1.0.0
* @var array
*/
protected array $span_trigger = array(
'colspan' => '#colspan#',
'rowspan' => '#rowspan#',
'span' => '#span#',
);
/**
* Buffer to store the counts of rowspan per column, initialized in _render_table().
*
* @since 1.0.0
* @var int[]
*/
protected array $rowspan = array();
/**
* Buffer to store the counts of colspan per row, initialized in _render_table().
*
* @since 1.0.0
* @var int[]
*/
protected array $colspan = array();
/**
* Whether the table has connected cells (colspan or rowspan), set in _render_table().
*
* @since 3.0.0
*/
protected bool $tbody_has_connected_cells = false;
/**
* Index of the last row of the visible data in the table, set in _render_table().
*
* @since 1.0.0
*/
protected int $last_row_idx;
/**
* Index of the last column of the visible data in the table, set in _render_table().
*
* @since 1.0.0
*/
protected int $last_column_idx;
/**
* Class constructor.
*
* @since 1.0.0
*/
public function __construct() {
// Unused.
}
/**
* Set the table (data, options, visibility, ...) that is to be rendered.
*
* @since 1.0.0
*
* @param array $table Table to be rendered.
* @param array $render_options Options for rendering, from both "Edit" screen and Shortcode.
*/
public function set_input( array $table, array $render_options ): void {
$this->table = $table;
$this->render_options = $render_options;
/**
* Filters the table before the render process.
*
* @since 1.0.0
*
* @param array $table The table.
* @param array $render_options The render options for the table.
*/
$this->table = apply_filters( 'tablepress_table_raw_render_data', $this->table, $this->render_options );
}
/**
* Process the table rendering and return the HTML output.
*
* @since 1.0.0
* @since 2.0.0 Add the $format parameter.
*
* @param string $format Optional. Output format, 'html' (default) or 'array'.
* @return string|array> HTML code of the rendered table, or a PHP array, or an error message.
*/
public function get_output( string $format = 'html' ) /* : string|array */ {
// Evaluate math expressions/formulas.
$this->_evaluate_table_data();
// Remove hidden rows and columns.
$this->_prepare_render_data();
if ( 'html' !== $format ) {
add_filter( 'tablepress_cell_content', 'wptexturize' );
}
// Evaluate Shortcodes and escape cell content.
$this->_process_render_data();
if ( 'html' !== $format ) {
remove_filter( 'tablepress_cell_content', 'wptexturize' );
}
switch ( $format ) {
case 'html':
$this->_render_table();
break;
case 'array':
$this->output = $this->table['data'];
break;
}
return $this->output;
}
/**
* Loop through the table to evaluate math expressions/formulas.
*
* @since 1.0.0
*/
protected function _evaluate_table_data(): void {
$orig_table = $this->table;
if ( $this->render_options['evaluate_formulas'] ) {
$formula_evaluator = TablePress::load_class( 'TablePress_Evaluate', 'class-evaluate.php', 'classes' );
$this->table['data'] = $formula_evaluator->evaluate_table_data( $this->table['data'], $this->table['id'] );
}
/**
* Filters the table after evaluating formulas in the table.
*
* @since 1.0.0
*
* @param array $table The table with evaluated formulas.
* @param array $orig_table The table with unevaluated formulas.
* @param array $render_options The render options for the table.
*/
$this->table = apply_filters( 'tablepress_table_evaluate_data', $this->table, $orig_table, $this->render_options );
}
/**
* Remove all cells from the data set that shall not be rendered, because they are hidden.
*
* @since 1.0.0
*/
protected function _prepare_render_data(): void {
$orig_table = $this->table;
$num_rows = count( $this->table['data'] );
$num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
// Evaluate show/hide_rows/columns parameters.
$actions = array( 'show', 'hide' );
$elements = array( 'rows', 'columns' );
foreach ( $actions as $action ) {
foreach ( $elements as $element ) {
if ( empty( $this->render_options[ "{$action}_{$element}" ] ) ) {
$this->render_options[ "{$action}_{$element}" ] = array();
continue;
}
// Add all rows/columns to array if "all" value set for one of the four parameters.
if ( 'all' === $this->render_options[ "{$action}_{$element}" ] ) {
$this->render_options[ "{$action}_{$element}" ] = range( 0, ${'num_' . $element} - 1 );
continue;
}
// We have a list of rows/columns (possibly with ranges in it).
$this->render_options[ "{$action}_{$element}" ] = explode( ',', $this->render_options[ "{$action}_{$element}" ] );
// Support for ranges like 3-6 or A-BA.
$range_cells = array();
foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
$range_dash = strpos( $value, '-' );
if ( false !== $range_dash ) {
unset( $this->render_options[ "{$action}_{$element}" ][ $key ] );
$start = trim( substr( $value, 0, $range_dash ) );
if ( ! is_numeric( $start ) ) {
$start = TablePress::letter_to_number( $start );
}
$end = trim( substr( $value, $range_dash + 1 ) );
if ( ! is_numeric( $end ) ) {
$end = TablePress::letter_to_number( $end );
}
$current_range = range( $start, $end );
$range_cells = array_merge( $range_cells, $current_range );
}
}
$this->render_options[ "{$action}_{$element}" ] = array_merge( $this->render_options[ "{$action}_{$element}" ], $range_cells );
/*
* Parse single letters and change from regular numbering to zero-based numbering,
* as rows/columns are indexed from 0 internally, but from 1 externally.
*/
foreach ( $this->render_options[ "{$action}_{$element}" ] as $key => $value ) {
$value = trim( $value );
if ( ! is_numeric( $value ) ) {
$value = TablePress::letter_to_number( $value );
}
$this->render_options[ "{$action}_{$element}" ][ $key ] = (int) $value - 1;
}
// Remove duplicate entries and sort the array.
$this->render_options[ "{$action}_{$element}" ] = array_unique( $this->render_options[ "{$action}_{$element}" ] );
sort( $this->render_options[ "{$action}_{$element}" ], SORT_NUMERIC );
}
}
// Load information about hidden rows and columns.
// Get indexes of hidden rows (array value of 0).
$hidden_rows = array_keys( $this->table['visibility']['rows'], 0, true );
$hidden_rows = array_merge( $hidden_rows, $this->render_options['hide_rows'] );
$hidden_rows = array_diff( $hidden_rows, $this->render_options['show_rows'] );
// Get indexes of hidden columns (array value of 0).
$hidden_columns = array_keys( $this->table['visibility']['columns'], 0, true );
$hidden_columns = array_merge( $hidden_columns, $this->render_options['hide_columns'] );
$hidden_columns = array_merge( array_diff( $hidden_columns, $this->render_options['show_columns'] ) );
// Remove hidden rows and re-index.
foreach ( $hidden_rows as $row_idx ) {
unset( $this->table['data'][ $row_idx ] );
}
$this->table['data'] = array_merge( $this->table['data'] );
// Remove hidden columns and re-index.
foreach ( $this->table['data'] as $row_idx => $row ) {
foreach ( $hidden_columns as $col_idx ) {
unset( $row[ $col_idx ] );
}
$this->table['data'][ $row_idx ] = array_merge( $row );
}
/**
* Filters the table after processing the table visibility information.
*
* @since 1.0.0
*
* @param array $table The processed table.
* @param array $orig_table The unprocessed table.
* @param array $render_options The render options for the table.
*/
$this->table = apply_filters( 'tablepress_table_render_data', $this->table, $orig_table, $this->render_options );
}
/**
* Generate the data that is to be rendered.
*
* @since 2.0.0
*/
protected function _process_render_data(): void {
$orig_table = $this->table;
// Deactivate nl2br() for this render process, if "convert_line_breaks" Shortcode parameter is set to false.
if ( ! $this->render_options['convert_line_breaks'] ) {
add_filter( 'tablepress_apply_nl2br', '__return_false', 9 ); // Priority 9, so that this filter can easily be overwritten at the default priority.
}
foreach ( $this->table['data'] as $row_idx => $row ) {
foreach ( $row as $col_idx => $cell_content ) {
// Print formulas that are escaped with '= (like in Excel) as text.
if ( str_starts_with( $cell_content, "'=" ) ) {
$cell_content = substr( $cell_content, 1 );
}
$cell_content = $this->safe_output( $cell_content );
if ( str_contains( $cell_content, '[' ) ) {
$cell_content = do_shortcode( $cell_content );
}
/**
* Filters the content of a single cell, after formulas have been evaluated, the output has been sanitized, and Shortcodes have been evaluated.
*
* @since 1.0.0
*
* @param string $cell_content The cell content.
* @param string $table_id The current table ID.
* @param int $row_idx The row number of the cell.
* @param int $col_idx The column number of the cell.
*/
$cell_content = apply_filters( 'tablepress_cell_content', $cell_content, $this->table['id'], $row_idx + 1, $col_idx + 1 );
$this->table['data'][ $row_idx ][ $col_idx ] = $cell_content;
}
}
// Re-instate nl2br() behavior after this render process, if "convert_line_breaks" Shortcode parameter is set to false.
if ( ! $this->render_options['convert_line_breaks'] ) {
remove_filter( 'tablepress_apply_nl2br', '__return_false', 9 ); // Priority 9, so that this filter can easily be overwritten at the default priority.
}
/**
* Filters the table after processing the table content handling.
*
* @since 2.0.0
*
* @param array $table The processed table.
* @param array $orig_table The unprocessed table.
* @param array $render_options The render options for the table.
*/
$this->table = apply_filters( 'tablepress_table_content_render_data', $this->table, $orig_table, $this->render_options );
}
/**
* Generate the HTML output of the table.
*
* @since 1.0.0
*/
protected function _render_table(): void {
$num_rows = count( $this->table['data'] );
$num_columns = ( $num_rows > 0 ) ? count( $this->table['data'][0] ) : 0;
// Check if there are rows and columns in the table (might not be the case after removing hidden rows/columns!).
if ( 0 === $num_rows || 0 === $num_columns ) {
$this->output = sprintf( __( '', 'tablepress' ), $this->table['id'] );
return;
}
// Counters for spans of rows and columns, init to 1 for each row and column (as that means no span).
$this->rowspan = array_fill( 0, $num_columns, 1 );
$this->colspan = array_fill( 0, $num_rows, 1 );
/**
* Filters the trigger keywords for "colspan" and "rowspan"
*
* @since 1.0.0
*
* @param array $span_trigger The trigger keywords for combining table cells.
* @param string $table_id The current table ID.
*/
$this->span_trigger = apply_filters( 'tablepress_span_trigger_keywords', $this->span_trigger, $this->table['id'] );
// Explode from string to array.
$this->render_options['column_widths'] = ( ! empty( $this->render_options['column_widths'] ) ) ? explode( '|', $this->render_options['column_widths'] ) : array();
// Make array $this->render_options['column_widths'] have $columns entries.
$this->render_options['column_widths'] = array_pad( $this->render_options['column_widths'], $num_columns, '' );
$output = '';
if ( $this->render_options['print_name'] ) {
/**
* Filters the HTML tag that wraps the printed table name.
*
* @since 1.0.0
*
* @param string $tag The HTML tag around the table name. Default h2.
* @param string $table_id The current table ID.
*/
$name_html_tag = apply_filters( 'tablepress_print_name_html_tag', 'h2', $this->table['id'] );
$name_attributes = array();
if ( ! empty( $this->render_options['html_id'] ) ) {
$name_attributes['id'] = "{$this->render_options['html_id']}-name";
}
/**
* Filters the class attribute for the printed table name.
*
* @since 1.0.0
* @deprecated 1.13.0 Use {@see 'tablepress_table_name_tag_attributes'} instead.
*
* @param string $class The class attribute for the table name that can be used in CSS code.
* @param string $table_id The current table ID.
*/
$name_attributes['class'] = apply_filters_deprecated( 'tablepress_print_name_css_class', array( "tablepress-table-name tablepress-table-name-id-{$this->table['id']}", $this->table['id'] ), 'TablePress 1.13.0', 'tablepress_table_name_tag_attributes' );
/**
* Filters the attributes for the table name (HTML h2 element, by default).
*
* @since 1.13.0
*
* @param array $name_attributes The attributes for the table name element.
* @param array $table The current table.
* @param array $render_options The render options for the table.
*/
$name_attributes = apply_filters( 'tablepress_table_name_tag_attributes', $name_attributes, $this->table, $this->render_options );
$name_attributes = $this->_attributes_array_to_string( $name_attributes );
$print_name_html = "<{$name_html_tag}{$name_attributes}>" . $this->safe_output( $this->table['name'] ) . "{$name_html_tag}>\n";
}
if ( $this->render_options['print_description'] ) {
/**
* Filters the HTML tag that wraps the printed table description.
*
* @since 1.0.0
*
* @param string $tag The HTML tag around the table description. Default span.
* @param string $table_id The current table ID.
*/
$description_html_tag = apply_filters( 'tablepress_print_description_html_tag', 'span', $this->table['id'] );
$description_attributes = array();
if ( ! empty( $this->render_options['html_id'] ) ) {
$description_attributes['id'] = "{$this->render_options['html_id']}-description";
}
/**
* Filters the class attribute for the printed table description.
*
* @since 1.0.0
* @deprecated 1.13.0 Use {@see 'tablepress_table_description_tag_attributes'} instead.
*
* @param string $class The class attribute for the table description that can be used in CSS code.
* @param string $table_id The current table ID.
*/
$description_attributes['class'] = apply_filters_deprecated( 'tablepress_print_description_css_class', array( "tablepress-table-description tablepress-table-description-id-{$this->table['id']}", $this->table['id'] ), 'TablePress 1.13.0', 'tablepress_table_description_tag_attributes' );
/**
* Filters the attributes for the table description (HTML span element, by default).
*
* @since 1.13.0
*
* @param array $description_attributes The attributes for the table description element.
* @param array $table The current table.
* @param array $render_options The render options for the table.
*/
$description_attributes = apply_filters( 'tablepress_table_description_tag_attributes', $description_attributes, $this->table, $this->render_options );
$description_attributes = $this->_attributes_array_to_string( $description_attributes );
$print_description_html = "<{$description_html_tag}{$description_attributes}>" . $this->safe_output( $this->table['description'] ) . "{$description_html_tag}>\n";
}
if ( $this->render_options['print_name'] && 'above' === $this->render_options['print_name_position'] ) {
$output .= $print_name_html;
}
if ( $this->render_options['print_description'] && 'above' === $this->render_options['print_description_position'] ) {
$output .= $print_description_html;
}
$thead = array();
$tfoot = array();
$tbody = array();
$this->last_row_idx = $num_rows - 1;
$this->last_column_idx = $num_columns - 1;
// Loop through rows in reversed order, to search for rowspan trigger keyword.
$row_idx = $this->last_row_idx;
// Render the table footer rows, if there is at least one extra row.
if ( $this->render_options['table_foot'] > 0 && $num_rows >= $this->render_options['table_head'] + $this->render_options['table_foot'] ) { // @phpstan-ignore greaterOrEqual.invalid (`table_head` and `table_foot` are integers.)
$last_tbody_idx = $this->last_row_idx - $this->render_options['table_foot'];
while ( $row_idx > $last_tbody_idx ) {
$tfoot[] = $this->_render_row( $row_idx, 'th' );
--$row_idx;
}
// Reverse rows because we looped through the rows in reverse order.
$tfoot = array_reverse( $tfoot );
}
// Render the table body rows.
$last_thead_idx = $this->render_options['table_head'] - 1;
while ( $row_idx > $last_thead_idx ) {
$tbody[] = $this->_render_row( $row_idx, 'td' );
--$row_idx;
}
// Reverse rows because we looped through the rows in reverse order.
$tbody = array_reverse( $tbody );
// Render the table header rows, if rows are left.
while ( $row_idx > -1 ) {
$thead[] = $this->_render_row( $row_idx, 'th' );
--$row_idx;
}
// Reverse rows because we looped through the rows in reverse order.
$thead = array_reverse( $thead );
//
tag.
/**
* Filters the content for the HTML caption element of the table.
*
* If the "Edit" link for a table is shown, it is also added to the caption element.
*
* @since 1.0.0
*
* @param string $caption The content for the HTML caption element of the table. Default empty.
* @param array $table The current table.
*/
$caption = apply_filters( 'tablepress_print_caption_text', '', $this->table );
$caption_style = '';
$caption_class = '';
if ( ! empty( $caption ) ) {
/**
* Filters the class attribute for the HTML caption element of the table.
*
* @since 1.0.0
*
* @param string $class The class attribute for the HTML caption element of the table.
* @param string $table_id The current table ID.
*/
$caption_class = apply_filters( 'tablepress_print_caption_class', "tablepress-table-caption tablepress-table-caption-id-{$this->table['id']}", $this->table['id'] );
$caption_class = ' class="' . $caption_class . '"';
}
if ( ! empty( $this->render_options['edit_table_url'] ) ) {
if ( empty( $caption ) ) {
$caption_style = ' style="caption-side:bottom;text-align:left;border:none;background:none;margin:0;padding:0;"';
} else {
$caption .= ' ';
}
$caption .= '' . __( 'Edit', 'default' ) . '';
}
if ( ! empty( $caption ) ) {
$caption = "
{$caption}
\n";
}
//
tag.
$colgroup = '';
/**
* Filters whether the HTML colgroup tag shall be added to the table output.
*
* @since 1.0.0
*
* @param bool $print Whether the colgroup element shall be printed.
* @param string $table_id The current table ID.
*/
if ( apply_filters( 'tablepress_print_colgroup_tag', false, $this->table['id'] ) ) {
for ( $col_idx = 0; $col_idx < $num_columns; $col_idx++ ) {
$attributes = ' class="colgroup-column-' . ( $col_idx + 1 ) . ' "';
/**
* Filters the attributes of the HTML col tags in the HTML colgroup tag.
*
* @since 1.0.0
*
* @param string $attributes The attributes in the col element.
* @param string $table_id The current table ID.
* @param int $col_idx The number of the column.
*/
$attributes = apply_filters( 'tablepress_colgroup_tag_attributes', $attributes, $this->table['id'], $col_idx + 1 );
$colgroup .= "\t
';
/**
* Filters the generated HTML code for the table, without HTML elements around it.
*
* @since 2.4.0
*
* @param string $output The generated HTML for the table, without HTML elements around it.
* @param array $table The current table.
* @param array $render_options The render options for the table, without HTML elements around it.
*/
$table_html = apply_filters( 'tablepress_table_html', $table_html, $this->table, $this->render_options );
$output .= "\n{$table_html}\n";
unset( $table_html ); // Unset the potentially large variable to free up memory.
// name/description below table (HTML already generated above).
if ( $this->render_options['print_name'] && 'below' === $this->render_options['print_name_position'] ) {
$output .= $print_name_html; // @phpstan-ignore variable.undefined (The variable is set above.)
}
if ( $this->render_options['print_description'] && 'below' === $this->render_options['print_description_position'] ) {
$output .= $print_description_html; // @phpstan-ignore variable.undefined (The variable is set above.)
}
/**
* Filters the generated HTML code for the table and HTML elements around it.
*
* @since 1.0.0
*
* @param string $output The generated HTML for the table and HTML elements around it.
* @param array $table The current table.
* @param array $render_options The render options for the table and HTML elements around it.
*/
$this->output = apply_filters( 'tablepress_table_output', $output, $this->table, $this->render_options );
}
/**
* Generate the HTML of a row.
*
* @since 1.0.0
*
* @param int $row_idx Index of the row to be rendered.
* @param string $tag HTML tag to use for the cells (td or th).
* @return string HTML for the row.
*/
protected function _render_row( int $row_idx, string $tag ): string {
$row_cells = array();
// Loop through cells in reversed order, to search for colspan or rowspan trigger words.
for ( $col_idx = $this->last_column_idx; $col_idx >= 0; $col_idx-- ) {
$cell_content = $this->table['data'][ $row_idx ][ $col_idx ];
if ( $this->span_trigger['rowspan'] === $cell_content ) { // There will be a rowspan.
if ( ! (
( 0 === $row_idx ) // No rowspan inside first row.
|| ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
|| ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
) ) {
// Increase counter for rowspan in this column.
++$this->rowspan[ $col_idx ];
// Reset counter for colspan in this row, combined col- and rowspan might be happening.
$this->colspan[ $row_idx ] = 1;
continue;
}
// Invalid rowspan, so we set cell content from #rowspan# to empty.
$cell_content = '';
} elseif ( $this->span_trigger['colspan'] === $cell_content ) { // There will be a colspan.
if ( ! (
( 0 === $col_idx ) // No colspan inside first column.
|| ( 1 === $col_idx && $this->render_options['first_column_th'] ) // No colspan into first column head.
) ) {
// Increase counter for colspan in this row.
++$this->colspan[ $row_idx ];
// Reset counter for rowspan in this column, combined col- and rowspan might be happening.
$this->rowspan[ $col_idx ] = 1;
continue;
}
// Invalid colspan, so we set cell content from #colspan# to empty.
$cell_content = '';
} elseif ( $this->span_trigger['span'] === $cell_content ) { // There will be a combined col- and rowspan.
if ( ! (
( 0 === $row_idx ) // No rowspan inside first row.
|| ( $this->render_options['table_head'] === $row_idx ) // No rowspan into table head.
|| ( $this->last_row_idx - $this->render_options['table_foot'] + 1 === $row_idx ) // No rowspan out of table foot.
) && ! (
( 0 === $col_idx ) // No colspan inside first column.
|| ( 1 === $col_idx && $this->render_options['first_column_th'] ) // No colspan into first column head.
) ) {
continue;
}
// Invalid span, so we set cell content from #span# to empty.
$cell_content = '';
}
// Attributes for the table cell (HTML td or th element).
$tag_attributes = array();
// "colspan" and "rowspan" attributes.
if ( $this->colspan[ $row_idx ] > 1 ) { // We have colspaned cells.
$tag_attributes['colspan'] = (string) $this->colspan[ $row_idx ];
if ( ! $this->tbody_has_connected_cells && $row_idx > $this->render_options['table_head'] - 1 && $row_idx < $this->last_row_idx - $this->render_options['table_foot'] + 1 ) {
// Set flag that there are connected cells in the tbody.
$this->tbody_has_connected_cells = true;
}
}
if ( $this->rowspan[ $col_idx ] > 1 ) { // We have rowspaned cells.
$tag_attributes['rowspan'] = (string) $this->rowspan[ $col_idx ];
if ( ! $this->tbody_has_connected_cells && $row_idx > $this->render_options['table_head'] - 1 && $row_idx < $this->last_row_idx - $this->render_options['table_foot'] + 1 ) {
// Set flag that there are connected cells in the tbody.
$this->tbody_has_connected_cells = true;
}
}
// "class" attribute.
$cell_class = 'column-' . ( $col_idx + 1 );
/**
* Filters the CSS classes that are given to a single cell (HTML td element) of a table.
*
* @since 1.0.0
*
* @param string $cell_class The CSS classes for the cell.
* @param string $table_id The current table ID.
* @param string $cell_content The cell content.
* @param int $row_idx The row number of the cell.
* @param int $col_idx The column number of the cell.
* @param int $colspan_row The number of combined columns for this cell.
* @param int $rowspan_col The number of combined rows for this cell.
*/
$cell_class = apply_filters( 'tablepress_cell_css_class', $cell_class, $this->table['id'], $cell_content, $row_idx + 1, $col_idx + 1, $this->colspan[ $row_idx ], $this->rowspan[ $col_idx ] );
if ( ! empty( $cell_class ) ) {
$tag_attributes['class'] = $cell_class;
}
// "style" attribute.
if ( ( 0 === $row_idx ) && ! empty( $this->render_options['column_widths'][ $col_idx ] ) ) {
$tag_attributes['style'] = 'width:' . preg_replace( '#[^0-9a-z.%]#', '', $this->render_options['column_widths'][ $col_idx ] ) . ';';
}
/**
* Filters the attributes for the table cell (HTML td or th element).
*
* @since 1.4.0
*
* @param array $tag_attributes The attributes for the td or th element.
* @param string $table_id The current table ID.
* @param string $cell_content The cell content.
* @param int $row_idx The row number of the cell.
* @param int $col_idx The column number of the cell.
* @param int $colspan_row The number of combined columns for this cell.
* @param int $rowspan_col The number of combined rows for this cell.
*/
$tag_attributes = apply_filters( 'tablepress_cell_tag_attributes', $tag_attributes, $this->table['id'], $cell_content, $row_idx + 1, $col_idx + 1, $this->colspan[ $row_idx ], $this->rowspan[ $col_idx ] );
$tag_attributes = $this->_attributes_array_to_string( $tag_attributes );
if ( '' === $cell_content ) {
$cell_tag = 'td'; // For accessibility, empty cells should use `td` and not `th` tags.
} elseif ( $this->render_options['first_column_th'] && 0 === $col_idx ) {
$cell_tag = 'th'; // Non-empty cells in the first column should use `th` tags, if enabled.
} else {
$cell_tag = $tag; // Otherwise, use the tag that was passed in as the default for the row.
}
$row_cells[] = "<{$cell_tag}{$tag_attributes}>{$cell_content}{$cell_tag}>";
$this->colspan[ $row_idx ] = 1; // Reset.
$this->rowspan[ $col_idx ] = 1; // Reset.
}
// Attributes for the table row (HTML tr element).
$tr_attributes = array();
// "class" attribute.
$row_classes = 'row-' . ( $row_idx + 1 );
/**
* Filters the CSS classes that are given to a row (HTML tr element) of a table.
*
* @since 1.0.0
*
* @param string $row_classes The CSS classes for the row.
* @param string $table_id The current table ID.
* @param string[] $row_cells The HTML code for the cells of the row.
* @param int $row_idx The row number.
* @param string[] $row_data The content of the cells of the row.
*/
$row_classes = apply_filters( 'tablepress_row_css_class', $row_classes, $this->table['id'], $row_cells, $row_idx + 1, $this->table['data'][ $row_idx ] );
if ( ! empty( $row_classes ) ) {
$tr_attributes['class'] = $row_classes;
}
/**
* Filters the attributes for the table row (HTML tr element).
*
* @since 1.4.0
*
* @param array $tr_attributes The attributes for the tr element.
* @param string $table_id The current table ID.
* @param int $row_idx The row number.
* @param string[] $row_data The content of the cells of the row.
*/
$tr_attributes = apply_filters( 'tablepress_row_tag_attributes', $tr_attributes, $this->table['id'], $row_idx + 1, $this->table['data'][ $row_idx ] );
$tr_attributes = $this->_attributes_array_to_string( $tr_attributes );
// Reverse rows because we looped through the cells in reverse order.
$row_cells = array_reverse( $row_cells );
return "
\n\t" . implode( '', $row_cells ) . "\n
\n";
}
/**
* Convert an array of HTML tag attributes to a string.
*
* @since 1.4.0
*
* @param array $attributes Attributes for the HTML tag in the array keys, and their values in the array values.
* @return string The attributes as a string for usage in a HTML element.
*/
protected function _attributes_array_to_string( array $attributes ): string {
$attributes_string = '';
foreach ( $attributes as $attribute => $value ) {
$attributes_string .= " {$attribute}=\"{$value}\"";
}
return $attributes_string;
}
/**
* Possibly replace certain HTML entities and replace line breaks with HTML.
*
* @since 1.0.0
*
* @param string $text The string to process.
* @return string Processed string for output.
*/
protected function safe_output( string $text ): string {
/*
* Replace any & with & that is not already an encoded entity (from function htmlentities2 in WP 2.8).
* A complete htmlentities2() or htmlspecialchars() would encode tags, which we don't want.
*/
$text = (string) preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&', $text );
/**
* Filters whether line breaks in the cell content shall be replaced with HTML br tags.
*
* @since 1.0.0
*
* @param bool $replace Whether to replace line breaks with HTML br tags. Default true.
* @param string $table_id The current table ID.
*/
if ( apply_filters( 'tablepress_apply_nl2br', true, $this->table['id'] ) ) {
$text = nl2br( $text );
}
return $text;
}
/**
* Get the default render options, null means: Use option from "Edit" screen.
*
* @since 1.0.0
*
* @return array Default render options.
*/
public function get_default_render_options(): array {
// Attention: Array keys have to be lowercase, otherwise they won't match the Shortcode attributes, which will be passed in lowercase by WP.
return array(
'alternating_row_colors' => null,
'block_preview' => false,
'border' => false,
'cache_table_output' => true,
'cellpadding' => false,
'cellspacing' => false,
'column_widths' => '',
'convert_line_breaks' => true,
'datatables_custom_commands' => null,
'datatables_datetime' => '',
'datatables_filter' => null,
'datatables_info' => null,
'datatables_lengthchange' => null,
'datatables_locale' => get_locale(),
'datatables_paginate' => null,
'datatables_paginate_entries' => null,
'datatables_scrollx' => null,
'datatables_scrolly' => false,
'datatables_sort' => null,
'evaluate_formulas' => true,
'extra_css_classes' => null,
'first_column_th' => false,
'hide_columns' => '',
'hide_rows' => '',
'id' => '',
'print_description' => null,
'print_description_position' => null,
'print_name' => null,
'print_name_position' => null,
'row_hover' => null,
'shortcode_debug' => false,
'show_columns' => '',
'show_rows' => '',
'table_foot' => null,
'table_head' => null,
'use_datatables' => null,
);
}
/**
* Get the CSS code for the Preview iframe.
*
* @since 1.0.0
*
* @return string CSS for the Preview iframe.
*/
public function get_preview_css(): string {
$is_rtl = is_rtl();
$tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
$default_css_minified = $tablepress_css->load_default_css_from_file( $is_rtl );
if ( false === $default_css_minified ) {
$default_css_minified = '';
}
$rtl_direction = $is_rtl ? "\ndirection: rtl;" : '';
return <<
/* iframe */
body {
margin: 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;{$rtl_direction}
}
p {
font-size: 13px;
}
{$default_css_minified}
CSS;
}
} // class TablePress_Render
class-evaluate-phpspreadsheet.php 0000644 00000014046 15021224011 0013174 0 ustar 00 > $table_data Table data in which formulas shall be evaluated.
* @param string $table_id ID of the passed table.
* @return array> Table data with evaluated formulas.
*/
public function evaluate_table_data( array $table_data, string $table_id ): array {
$table_has_formulas = false;
// Loop through all cells to check for formulas and convert notations.
foreach ( $table_data as &$row ) {
foreach ( $row as &$cell_content ) {
if ( '' === $cell_content || '=' === $cell_content || '=' !== $cell_content[0] ) {
continue;
}
$table_has_formulas = true;
// Convert legacy "formulas in text" notation (`=Text {A3+B3} Text`) to standard Excel notation (`="Text "&A3+B3&" Text"`).
if ( 1 === preg_match( '#{(.+?)}#', $cell_content ) ) {
$cell_content = str_replace( '"', '""', $cell_content ); // Preserve existing quotation marks in text around formulas.
$cell_content = '="' . substr( $cell_content, 1 ) . '"'; // Wrap the whole cell content in quotation marks, as there will be text around formulas.
$cell_content = (string) preg_replace( '#{(.+?)}#', '"&$1&"', $cell_content, -1, $count ); // Convert all wrapped formulas to standard Excel notation.
}
}
}
unset( $row, $cell_content ); // Unset use-by-reference parameters of foreach loops.
// No need to use the PHPSpreadsheet Calculation engine if the table does not contain formulas.
if ( ! $table_has_formulas ) {
return $table_data;
}
try {
$spreadsheet = new \TablePress\PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $spreadsheet->setActiveSheetIndex( 0 );
$worksheet->fromArray( /* $source */ $table_data, /* $nullValue */ '' );
// Don't allow cyclic references.
\TablePress\PhpOffice\PhpSpreadsheet\Calculation\Calculation::getInstance( $spreadsheet )->cyclicFormulaCount = 0;
/*
* Register variables as Named Formulas.
* The variables `ROW`, `COLUMN`, `CELL`, `PI`, and `E` should be considered deprecated and only their formulas should be used.
*/
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'TABLE_ID', $worksheet, $table_id ) );
$num_rows = (string) count( $table_data );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'NUM_ROWS', $worksheet, $num_rows ) );
$num_columns = (string) count( $table_data[0] );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'NUM_COLUMNS', $worksheet, $num_columns ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'ROW', $worksheet, '=ROW()' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'COLUMN', $worksheet, '=COLUMN()' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'CELL', $worksheet, '=ADDRESS(ROW(),COLUMN(),4)' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'PI', $worksheet, '=PI()' ) );
$spreadsheet->addNamedFormula( new \TablePress\PhpOffice\PhpSpreadsheet\NamedFormula( 'E', $worksheet, '=EXP(1)' ) );
// Loop through all table cells and replace formulas with evaluated values.
$cell_collection = $worksheet->getCellCollection();
foreach ( $table_data as $row_idx => &$row ) {
foreach ( $row as $column_idx => &$cell_content ) {
if ( strlen( $cell_content ) > 1 && '=' === $cell_content[0] ) {
// Adapted from \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::rangeToArray().
$cell_reference = \TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex( $column_idx + 1 ) . ( $row_idx + 1 );
if ( $cell_collection->has( $cell_reference ) ) {
$cell = $cell_collection->get( $cell_reference );
try {
$cell_content = (string) $cell->getCalculatedValue();
// Convert hyperlinks, e.g. generated via `=HYPERLINK()` to HTML code.
$cell_has_hyperlink = $worksheet->hyperlinkExists( $cell_reference ) && ! $worksheet->getHyperlink( $cell_reference )->isInternal();
if ( $cell_has_hyperlink ) {
$url = $worksheet->getHyperlink( $cell_reference )->getUrl();
if ( '' !== $url ) {
$url = esc_url( $url );
$cell_content = "{$cell_content}";
}
}
// Sanitize the output of the evaluated formula.
$cell_content = wp_kses_post( $cell_content ); // Equals wp_filter_post_kses(), but without the unnecessary slashes handling.
} catch ( \TablePress\PhpOffice\PhpSpreadsheet\Calculation\Exception $exception ) {
$message = str_replace( 'Worksheet!', '', $exception->getMessage() );
$cell_content = "!ERROR! {$message}";
}
}
}
}
}
unset( $row, $cell_content ); // Unset use-by-reference parameters of foreach loops.
// Save PHP memory.
$spreadsheet->disconnectWorksheets();
unset( $cell_collection, $worksheet, $spreadsheet );
} catch ( \TablePress\PhpOffice\PhpSpreadsheet\Calculation\Exception $exception ) {
$message = str_replace( 'Worksheet!', '', $exception->getMessage() );
$table_data = array( array( "!ERROR! {$message}" ) );
}
return $table_data;
}
} // class TablePress_Evaluate_PHPSpreadsheet
class-css.php 0000644 00000037746 15021224011 0007155 0 ustar 00 " with ">", but ">" is valid in CSS selectors.
$css = str_replace( '>', '>', $css );
// strip_tags again, because of the just added ">" (KSES for a second time would again bring the ">" problem).
$css = strip_tags( $css );
}
$csstidy->set_cfg( 'remove_bslash', false );
$csstidy->set_cfg( 'compress_colors', false );
$csstidy->set_cfg( 'compress_font-weight', false );
$csstidy->set_cfg( 'lowercase_s', false );
$csstidy->set_cfg( 'optimise_shorthands', false );
$csstidy->set_cfg( 'remove_last_;', false );
$csstidy->set_cfg( 'case_properties', false );
$csstidy->set_cfg( 'sort_properties', false );
$csstidy->set_cfg( 'sort_selectors', false );
$csstidy->set_cfg( 'discard_invalid_selectors', false );
$csstidy->set_cfg( 'discard_invalid_properties', true );
$csstidy->set_cfg( 'merge_selectors', false );
$csstidy->set_cfg( 'css_level', 'CSS3.0' );
$csstidy->set_cfg( 'preserve_css', true );
$csstidy->set_cfg( 'timestamp', false );
$csstidy->set_cfg( 'template', 'default' );
$csstidy->parse( $css );
return $csstidy->print->plain();
}
/**
* Minify a string of CSS code, that should have been sanitized/tidied before.
*
* @since 1.1.0
*
* @param string $css CSS code.
* @return string Minified CSS code.
*/
public function minify_css( string $css ): string {
$csstidy = TablePress::load_class( 'TablePress_CSSTidy', 'class.csstidy.php', 'libraries/csstidy' );
$csstidy->set_cfg( 'remove_bslash', false );
$csstidy->set_cfg( 'compress_colors', true );
$csstidy->set_cfg( 'compress_font-weight', true );
$csstidy->set_cfg( 'lowercase_s', false );
$csstidy->set_cfg( 'optimise_shorthands', 1 );
$csstidy->set_cfg( 'remove_last_;', true );
$csstidy->set_cfg( 'case_properties', false );
$csstidy->set_cfg( 'sort_properties', false );
$csstidy->set_cfg( 'sort_selectors', false );
$csstidy->set_cfg( 'discard_invalid_selectors', false );
$csstidy->set_cfg( 'discard_invalid_properties', true );
$csstidy->set_cfg( 'merge_selectors', false );
$csstidy->set_cfg( 'css_level', 'CSS3.0' );
$csstidy->set_cfg( 'preserve_css', false );
$csstidy->set_cfg( 'timestamp', false );
$csstidy->set_cfg( 'template', 'highest' );
$csstidy->parse( $css );
$css = $csstidy->print->plain();
// Remove all CSS comments from the minified CSS code, as CSSTidy does not remove those inside a CSS selector.
return preg_replace( '!/\*[^*]*\*+([^/][^*]*\*+)*/\n?!', '', $css );
}
/**
* Get the location (file path or URL) of the "Custom CSS" file, depending on whether it's a Multisite install or not.
*
* @since 1.0.0
*
* @param string $type "normal" version, "minified" version, or "combined" (with TablePress Default CSS) version.
* @param string $location "path" or "url", for file path or URL.
* @return string Full file path or full URL for the "Custom CSS" file.
*/
public function get_custom_css_location( string $type, string $location ): string {
switch ( $type ) {
case 'combined':
$file = 'tablepress-combined.min.css';
break;
case 'minified':
$file = 'tablepress-custom.min.css';
break;
case 'normal':
default:
$file = 'tablepress-custom.css';
break;
}
if ( is_multisite() ) {
// Multisite installation: Use /wp-content/uploads/sites//.
$upload_location = wp_upload_dir();
} else {
// Singlesite installation: Use /wp-content/.
$upload_location = array(
'basedir' => WP_CONTENT_DIR,
'baseurl' => content_url(),
);
}
switch ( $location ) {
case 'url':
$url = set_url_scheme( $upload_location['baseurl'] . '/' . $file );
/**
* Filters the URL from which the "Custom CSS" file is loaded.
*
* @since 1.0.0
*
* @param string $url URL of the "Custom CSS" file.
* @param string $file File name of the "Custom CSS" file.
* @param string $type Type of the "Custom CSS" file ("normal", "minified", or "combined").
*/
$url = apply_filters( 'tablepress_custom_css_url', $url, $file, $type );
return $url;
// break; // unreachable.
case 'path':
$path = $upload_location['basedir'] . '/' . $file;
/**
* Filters the file path on the server from which the "Custom CSS" file is loaded.
*
* @since 1.0.0
*
* @param string $path File path of the "Custom CSS" file.
* @param string $file File name of the "Custom CSS" file.
* @param string $type Type of the "Custom CSS" file ("normal", "minified", or "combined").
*/
$path = apply_filters( 'tablepress_custom_css_file_name', $path, $file, $type );
return $path;
// break; // unreachable.
default:
// Return an empty string if no valid location was provided.
return '';
// break; // unreachable.
}
}
/**
* Load the contents of the file with the "Custom CSS".
*
* @since 1.0.0
*
* @param string $type Optional. Whether to load "normal" version or "minified" version. Default "normal".
* @return string|false Custom CSS on success, false on error.
*/
public function load_custom_css_from_file( string $type = 'normal' ) /* : string|false */ {
$filename = $this->get_custom_css_location( $type, 'path' );
// Check if file name is valid (0 means yes).
if ( 0 !== validate_file( $filename ) ) {
return false;
}
if ( ! @is_file( $filename ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
return false;
}
if ( ! @is_readable( $filename ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
return false;
}
return file_get_contents( $filename );
}
/**
* Loads the contents of the file with the TablePress Default CSS,
* either for left-to-right (LTR) or right-to-left (RTL), in minified form.
*
* @since 1.1.0
* @since 2.0.0 Added the `$load_rtl` parameter.
*
* @param bool $load_rtl Optional. Whether to load LTR or RTL version. Default LTR.
* @return string|false TablePress Default CSS on success, false on error.
*/
public function load_default_css_from_file( bool $load_rtl = false ) /* : string|false */ {
$rtl = $load_rtl ? '-rtl' : '';
$filename = TABLEPRESS_ABSPATH . "css/build/default{$rtl}.css";
// Check if file name is valid (0 means yes).
if ( 0 !== validate_file( $filename ) ) {
return false;
}
if ( ! @is_file( $filename ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
return false;
}
if ( ! @is_readable( $filename ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
return false;
}
return file_get_contents( $filename );
}
/**
* Try to save "Custom CSS" to a file (requires "direct" method in WP_Filesystem, or stored FTP credentials).
*
* @since 1.1.0
*
* @param string $custom_css_normal Custom CSS code to be saved.
* @param string $custom_css_minified Minified CSS code to be saved.
* @return bool True on success, false on failure.
*/
public function save_custom_css_to_file( string $custom_css_normal, string $custom_css_minified ): bool {
/**
* Filters whether the "Custom CSS" code shall be saved to a file on the server.
*
* @since 1.1.0
*
* @param bool $save Whether to save the "Custom CSS" to a file. Default true.
*/
if ( ! apply_filters( 'tablepress_save_custom_css_to_file', true ) ) {
return false;
}
// Start capturing the output, to later prevent it.
ob_start();
$credentials = request_filesystem_credentials( '', '', false, '', null, false );
/*
* Do we have credentials already? (Otherwise the form will have been rendered, which is not supported here.)
* Or, if we have credentials, are they valid?
*/
if ( false === $credentials || ! WP_Filesystem( $credentials ) ) { // @phpstan-ignore argument.type
ob_end_clean();
return false;
}
// We have valid access to the filesystem now -> try to save the files.
return $this->_custom_css_save_helper( $custom_css_normal, $custom_css_minified );
}
/**
* Save "Custom CSS" to files, delete "Custom CSS" files, or return HTML for the credentials form.
*
* Only used from "Plugin Options" screen, save_custom_css_to_file() is used in cases where no form output/redirection is possible (e.g. during plugin updates).
*
* @since 1.0.0
*
* @param string $custom_css_normal Custom CSS code to be saved. If empty, files will be deleted.
* @param string $custom_css_minified Minified CSS code to be saved.
* @return bool|string True on success, false on failure, or string of HTML for the credentials form for the WP_Filesystem API, if necessary.
*/
public function save_custom_css_to_file_plugin_options( string $custom_css_normal, string $custom_css_minified ) /* : bool|string */ {
/** This filter is documented in classes/class-css.php */
if ( ! apply_filters( 'tablepress_save_custom_css_to_file', true ) ) {
return false;
}
// Start capturing the output, to get HTML of the credentials form (if needed).
ob_start();
$credentials = request_filesystem_credentials( '', '', false, '', null, false );
// Do we have credentials already? Otherwise the form will have been rendered already.
if ( false === $credentials ) {
$form_data = ob_get_clean();
$form_data = str_replace( 'name="upgrade" id="upgrade" class="button"', 'name="upgrade" id="upgrade" class="components-button is-primary"', $form_data ); // @phpstan-ignore argument.type
return $form_data;
}
// We have received credentials, but don't know if they are valid yet.
if ( ! WP_Filesystem( $credentials ) ) { // @phpstan-ignore argument.type
// Credentials failed, so ask again (with $error flag true).
request_filesystem_credentials( '', '', true, '', null, false );
$form_data = ob_get_clean();
$form_data = str_replace( 'name="upgrade" id="upgrade" class="button"', 'name="upgrade" id="upgrade" class="components-button is-primary"', $form_data ); // @phpstan-ignore argument.type
return $form_data;
}
// We have valid access to the filesystem now -> try to save the files, or delete them if the "Custom CSS" is empty.
if ( '' !== $custom_css_normal ) {
return $this->_custom_css_save_helper( $custom_css_normal, $custom_css_minified );
} else {
return $this->_custom_css_delete_helper();
}
}
/**
* Save "Custom CSS" to files, if validated access to the WP_Filesystem exists.
*
* Helper function to prevent code duplication.
*
* @since 1.1.0
*
* @global WP_Filesystem_* $wp_filesystem WordPress file system abstraction object.
* @see save_custom_css_to_file()
* @see save_custom_css_to_file_plugin_options()
*
* @param string $custom_css_normal Custom CSS code to be saved.
* @param string $custom_css_minified Minified CSS code to be saved.
* @return bool True on success, false on failure.
*/
protected function _custom_css_save_helper( string $custom_css_normal, string $custom_css_minified ): bool {
global $wp_filesystem;
/*
* WP_CONTENT_DIR and (FTP-)Content-Dir can be different (e.g. if FTP working dir is /).
* We need to account for that by replacing the path difference in the filename.
*/
$path_difference = str_replace( $wp_filesystem->wp_content_dir(), '', trailingslashit( WP_CONTENT_DIR ) );
$css_types = array( 'normal', 'minified', 'combined' );
$default_css_minified = $this->load_default_css_from_file( false );
if ( false === $default_css_minified ) {
$default_css_minified = '';
}
$file_content = array(
'normal' => $custom_css_normal,
'minified' => $custom_css_minified,
'combined' => $default_css_minified . $custom_css_minified,
);
$total_result = true; // Whether all files were saved successfully.
foreach ( $css_types as $css_type ) {
$filename = $this->get_custom_css_location( $css_type, 'path' );
// Check if filename is valid (0 means yes).
if ( 0 !== validate_file( $filename ) ) {
$total_result = false;
continue;
}
if ( '' !== $path_difference ) {
$filename = str_replace( $path_difference, '', $filename );
}
$result = $wp_filesystem->put_contents( $filename, $file_content[ $css_type ], FS_CHMOD_FILE );
$total_result = ( $total_result && $result );
}
$this->_flush_caching_plugins_css_minify_caches();
return $total_result;
}
/**
* Delete the "Custom CSS" files, if possible.
*
* @since 1.0.0
*
* @return bool True on success, false on failure.
*/
public function delete_custom_css_files(): bool {
// Start capturing the output, to later prevent it.
ob_start();
$credentials = request_filesystem_credentials( '', '', false, '', null, false );
/*
* Do we have credentials already? (Otherwise the form will have been rendered, which is not supported here.)
* Or, if we have credentials, are they valid?
*/
if ( false === $credentials || ! WP_Filesystem( $credentials ) ) { // @phpstan-ignore argument.type
ob_end_clean();
return false;
}
// We have valid access to the filesystem now -> try to delete the files.
return $this->_custom_css_delete_helper();
}
/**
* Delete "Custom CSS" files, if validated access to the WP_Filesystem exists.
*
* Helper function to prevent code duplication
*
* @since 1.1.0
*
* @global WP_Filesystem_* $wp_filesystem WordPress file system abstraction object.
* @see delete_custom_css_files()
* @see save_custom_css_to_file_plugin_options()
*
* @return bool True on success, false on failure.
*/
protected function _custom_css_delete_helper(): bool {
global $wp_filesystem;
/*
* WP_CONTENT_DIR and (FTP-)Content-Dir can be different (e.g. if FTP working dir is /).
* We need to account for that by replacing the path difference in the filename.
*/
$path_difference = str_replace( $wp_filesystem->wp_content_dir(), '', trailingslashit( WP_CONTENT_DIR ) );
$css_types = array( 'normal', 'minified', 'combined' );
$total_result = true; // Whether all files were deleted successfully.
foreach ( $css_types as $css_type ) {
$filename = $this->get_custom_css_location( $css_type, 'path' );
// Check if filename is valid (0 means yes).
if ( 0 !== validate_file( $filename ) ) {
$total_result = false;
continue;
}
if ( '' !== $path_difference ) {
$filename = str_replace( $path_difference, '', $filename );
}
// We have valid access to the filesystem now -> try to delete the file.
if ( $wp_filesystem->exists( $filename ) ) {
$result = $wp_filesystem->delete( $filename );
$total_result = ( $total_result && $result );
}
}
$this->_flush_caching_plugins_css_minify_caches();
return $total_result;
}
/**
* Flush the CSS minification caches of common caching plugins.
*
* @since 1.4.0
*/
protected function _flush_caching_plugins_css_minify_caches(): void {
/** This filter is documented in models/model-table.php */
if ( ! apply_filters( 'tablepress_flush_caching_plugins_caches', true ) ) {
return;
}
// W3 Total Cache.
if ( function_exists( 'w3tc_minify_flush' ) ) {
w3tc_minify_flush();
}
// WP Fastest Cache.
if ( isset( $GLOBALS['wp_fastest_cache'] ) && is_callable( array( $GLOBALS['wp_fastest_cache'], 'deleteCache' ) ) ) {
$GLOBALS['wp_fastest_cache']->deleteCache( true ); // @phpstan-ignore method.nonObject
}
}
} // class TablePress_CSS
class-import-base.php 0000644 00000003303 15021224011 0010565 0 ustar 00 > $an_array Two-dimensional array to be padded.
*/
public function pad_array_to_max_cols( array &$an_array ): void {
$max_columns = $this->count_max_columns( $an_array );
// Extend the array to at least one column.
$max_columns = max( 1, $max_columns );
array_walk(
$an_array,
static function ( array &$row, int $col_idx ) use ( $max_columns ): void {
$row = array_pad( $row, $max_columns, '' );
},
);
}
/**
* Get the highest number of columns in the rows.
*
* @since 1.0.0
*
* @param array> $an_array Two-dimensional array.
* @return int Highest number of columns in the rows of the array.
*/
protected function count_max_columns( array $an_array ): int {
$max_columns = 0;
foreach ( $an_array as $row_idx => $row ) {
$num_columns = count( $row );
$max_columns = max( $num_columns, $max_columns );
}
return $max_columns;
}
} // class TablePress_Import_Base
class-wp_option.php 0000644 00000010602 15021224011 0010361 0 ustar 00 option_name = $params['option_name'];
$option_value = $this->_get_option( $this->option_name, null );
if ( ! is_null( $option_value ) ) {
$this->option_value = (array) json_decode( $option_value, true );
} else {
$this->option_value = $params['default_value'];
}
}
/**
* Check if Option is set.
*
* @since 1.0.0
*
* @param string $name Name of the option to check.
* @return bool Whether the option is set.
*/
public function is_set( string $name ): bool {
return isset( $this->option_value[ $name ] );
}
/**
* Get a single Option, or get all Options.
*
* @since 1.0.0
*
* @param string|false $name Optional. Name of a single option to get, or false for all options.
* @param mixed $default_value Optional. Default value to return, if a single option $name does not exist.
* @return mixed Value of the retrieved option $name or $default_value if it does not exist, or all options.
*/
public function get( /* string|false */ $name = false, /* string|int|float|bool|array|null */ $default_value = null ) /* : string|int|float|bool|array|null */ {
if ( false === $name ) {
return $this->option_value;
}
// Single Option wanted.
if ( isset( $this->option_value[ $name ] ) ) {
return $this->option_value[ $name ];
} else {
return $default_value;
}
}
/**
* Update Option.
*
* @since 1.0.0
*
* @param array $new_options New options (name => value).
* @return bool True on success, false on failure.
*/
public function update( array $new_options ): bool {
$this->option_value = $new_options;
return $this->_update_option( $this->option_name, wp_json_encode( $this->option_value, TABLEPRESS_JSON_OPTIONS ) ); // @phpstan-ignore argument.type
}
/**
* Delete Option.
*
* @since 1.0.0
*
* @return bool True on success, false on failure.
*/
public function delete(): bool {
return $this->_delete_option( $this->option_name );
}
/*
* Internal functions mapping - This needs to be re-defined by child classes.
*/
/**
* Get the value of a WP Option with the WP Options API.
*
* @since 1.0.0
*
* @param string $option_name Name of the WP Option.
* @param mixed $default_value Default value of the WP Option.
* @return mixed Current value of the WP Option, or $default_value if it does not exist.
*/
protected function _get_option( string $option_name, /* string|int|float|bool|array|null */ $default_value ) /* : string|int|float|bool|array|null */ {
return get_option( $option_name, $default_value );
}
/**
* Update the value of a WP Option with the WP Options API.
*
* @since 1.0.0
*
* @param string $option_name Name of the WP Option.
* @param string $new_value New value of the WP Option.
* @return bool True on success, false on failure.
*/
protected function _update_option( string $option_name, string $new_value ): bool {
return update_option( $option_name, $new_value );
}
/**
* Delete a WP Option with the WP Options API.
*
* @since 1.0.0
*
* @param string $option_name Name of the WP Option.
* @return bool True on success, false on failure.
*/
protected function _delete_option( string $option_name ): bool {
return delete_option( $option_name );
}
} // class TablePress_WP_Option
class-evaluate-legacy.php 0000644 00000021220 15021224011 0011411 0 ustar 00 >
*/
protected array $table_data;
/**
* Storage for cell ranges that have been replaced in formulas.
*
* @since 1.0.0
* @var array
*/
protected array $known_ranges = array();
/**
* Initialize the Formula Evaluation class, include the EvalMath class.
*
* @since 1.0.0
*/
public function __construct() {
$this->evalmath = TablePress::load_class( 'EvalMath', 'evalmath.class.php', 'libraries' );
// Don't raise PHP warnings.
$this->evalmath->suppress_errors = true;
}
/**
* Evaluate formulas in the passed table.
*
* @since 1.0.0
*
* @param array> $table_data Table data in which formulas shall be evaluated.
* @param string $table_id ID of the passed table.
* @return array> Table data with evaluated formulas.
*/
public function evaluate_table_data( array $table_data, string $table_id ): array {
$this->table_data = $table_data;
$num_rows = count( $this->table_data );
// Exit early if there's no actual table data (e.g. after using the Row Filter module).
if ( 0 === $num_rows ) {
return $this->table_data;
}
$num_columns = count( $this->table_data[0] );
// Make fixed table data available as variables in formulas.
$this->evalmath->variables['table_id'] = $table_id;
$this->evalmath->variables['num_rows'] = $num_rows;
$this->evalmath->variables['num_columns'] = $num_columns;
// Use two for-loops instead of foreach here to be sure to always work on the "live" table data and not some in-memory copy.
for ( $row_idx = 0; $row_idx < $num_rows; $row_idx++ ) {
for ( $col_idx = 0; $col_idx < $num_columns; $col_idx++ ) {
$this->table_data[ $row_idx ][ $col_idx ] = $this->_evaluate_cell( $this->table_data[ $row_idx ][ $col_idx ], $row_idx, $col_idx );
}
}
return $this->table_data;
}
/**
* Parse and evaluate the content of a cell.
*
* @since 1.0.0
*
* @param string $content Content of a cell.
* @param int $row_idx Row index of the cell.
* @param int $col_idx Column index of the cell.
* @param string[] $parents Optional. List of cells that depend on this cell (to prevent circle references).
* @return string Result of the parsing/evaluation.
*/
protected function _evaluate_cell( string $content, int $row_idx, int $col_idx, array $parents = array() ): string {
if ( '' === $content || '=' === $content || '=' !== $content[0] ) {
return $content;
}
// Cut off the leading =.
$content = substr( $content, 1 );
// Support putting formulas in strings, like =Total: {A3+A4}.
$expressions = array();
if ( preg_match_all( '#{(.+?)}#', $content, $expressions, PREG_SET_ORDER ) ) {
$formula_in_string = true;
} else {
$formula_in_string = false;
// Fill array so that it has the same structure as if it came from preg_match_all().
$expressions[] = array( $content, $content );
}
foreach ( $expressions as $expression ) {
$orig_expression = $expression[0];
$expression = $expression[1];
$replaced_references = array();
$replaced_ranges = array();
// Remove all whitespace characters.
$expression = str_replace( array( "\n", "\r", "\t", ' ' ), '', $expression );
// Expand cell ranges (like A3:A6) to a list of single cells (like A3,A4,A5,A6).
if ( preg_match_all( '#([A-Z]+)([0-9]+):([A-Z]+)([0-9]+)#', $expression, $referenced_cell_ranges, PREG_SET_ORDER ) ) {
foreach ( $referenced_cell_ranges as $cell_range ) {
if ( in_array( $cell_range[0], $replaced_ranges, true ) ) {
continue;
}
$replaced_ranges[] = $cell_range[0];
if ( isset( $this->known_ranges[ $cell_range[0] ] ) ) {
$expression = (string) preg_replace( '#(?known_ranges[ $cell_range[0] ], $expression );
continue;
}
// No -1 necessary for this transformation, as we don't actually access the table.
$first_col = TablePress::letter_to_number( $cell_range[1] );
$first_row = (int) $cell_range[2];
$last_col = TablePress::letter_to_number( $cell_range[3] );
$last_row = (int) $cell_range[4];
$col_start = min( $first_col, $last_col );
$col_end = max( $first_col, $last_col ) + 1; // +1 for loop below
$row_start = min( $first_row, $last_row );
$row_end = max( $first_row, $last_row ) + 1; // +1 for loop below
$cell_list = array();
for ( $col = $col_start; $col < $col_end; $col++ ) {
for ( $row = $row_start; $row < $row_end; $row++ ) {
$column = TablePress::number_to_letter( $col );
$cell_list[] = "{$column}{$row}";
}
}
$cell_list = implode( ',', $cell_list );
$expression = (string) preg_replace( '#(?known_ranges[ $cell_range[0] ] = $cell_list;
}
}
// Parse and evaluate single cell references (like A3 or XY312), while prohibiting circle references.
if ( preg_match_all( '#([A-Z]+)([0-9]+)(?![0-9A-Z\(])#', $expression, $referenced_cells, PREG_SET_ORDER ) ) {
foreach ( $referenced_cells as $cell_reference ) {
if ( in_array( $cell_reference[0], $parents, true ) ) {
return '!ERROR! Circle Reference';
}
if ( in_array( $cell_reference[0], $replaced_references, true ) ) {
continue;
}
$replaced_references[] = $cell_reference[0];
$ref_col = TablePress::letter_to_number( $cell_reference[1] ) - 1;
$ref_row = (int) $cell_reference[2] - 1;
if ( ! isset( $this->table_data[ $ref_row ][ $ref_col ] ) ) {
return "!ERROR! Cell {$cell_reference[0]} does not exist";
}
$ref_parents = $parents;
$ref_parents[] = $cell_reference[0];
$result = $this->_evaluate_cell( $this->table_data[ $ref_row ][ $ref_col ], $ref_row, $ref_col, $ref_parents );
$this->table_data[ $ref_row ][ $ref_col ] = $result;
// Bail if there was an error already.
if ( str_contains( $result, '!ERROR!' ) ) {
return $result;
}
// Remove all whitespace characters.
$result = str_replace( array( "\n", "\r", "\t", ' ' ), '', $result );
// Treat empty cells as 0.
if ( '' === $result ) {
$result = '0';
}
// Bail if the cell does not result in a number (meaning it was a number or expression before being evaluated).
if ( ! is_numeric( $result ) ) {
return "!ERROR! {$cell_reference[0]} does not contain a number or expression";
}
$expression = (string) preg_replace( '#(?_evaluate_math_expression( $expression, $row_idx, $col_idx );
// Support putting formulas in strings, like =Total: {A3+A4}.
if ( $formula_in_string ) {
$content = str_replace( $orig_expression, $result, $content );
} else {
$content = $result;
}
}
return $content;
}
/**
* Evaluate a math expression.
*
* @since 1.0.0
*
* @param string $expression Math expression without leading = sign.
* @param int $row_idx Row index of the cell with the expression.
* @param int $col_idx Column index of the cell with the expression.
* @return string Result of the evaluation.
*/
protected function _evaluate_math_expression( string $expression, int $row_idx, int $col_idx ): string {
// Make current cell's name and row and column number available as variables in formulas.
$this->evalmath->variables['row'] = $row_idx + 1;
$this->evalmath->variables['column'] = $col_idx + 1;
$this->evalmath->variables['cell'] = TablePress::number_to_letter( $this->evalmath->variables['column'] ) . $this->evalmath->variables['row'];
// Straight up evaluation, without parsing of variable or function assignments (which is why we only need one instance of the object).
$result = $this->evalmath->evaluate( $expression );
if ( false === $result ) {
$result = '!ERROR! ' . $this->evalmath->last_error;
}
return (string) $result;
}
} // class TablePress_Evaluate_Legacy
class-import-phpspreadsheet.php 0000644 00000040436 15021224011 0012702 0 ustar 00 |WP_Error Table array on success, WP_Error on error.
*/
public function import_table( File $file ) /* : array|WP_Error */ {
$data = file_get_contents( $file->location );
if ( false === $data ) {
return new WP_Error( 'table_import_phpspreadsheet_data_read', '', $file->location );
}
// Remove a possible UTF-8 Byte-Order Mark (BOM).
$bom = pack( 'CCC', 0xef, 0xbb, 0xbf );
if ( str_starts_with( $data, $bom ) ) {
$data = substr( $data, 3 );
}
if ( '' === $data ) {
return new WP_Error( 'table_import_phpspreadsheet_data_empty', '', $file->location );
}
$table = $this->_maybe_import_json( $data );
if ( is_array( $table ) ) {
return $table;
}
$table = $this->_maybe_import_html( $data );
if ( is_array( $table ) ) {
return $table;
}
return $this->_import_phpspreadsheet( $file );
}
/**
* Tries to import a table with the JSON format.
*
* @since 2.0.0
*
* @param string $data Data to import.
* @return array|false Table array on success, false if the file is not a JSON file.
*/
protected function _maybe_import_json( string $data ) /* : array|false */ {
$data = trim( $data );
// If the file does not begin / end with [ / ] or { / }, it's not a supported JSON file.
$first_character = $data[0];
$last_character = $data[-1];
if ( ! ( '[' === $first_character && ']' === $last_character ) && ! ( '{' === $first_character && '}' === $last_character ) ) {
return false;
}
$json_table = json_decode( $data, true );
// Check if JSON could be decoded. If not, this is probably not a JSON file.
if ( is_null( $json_table ) ) {
return false;
}
// Specifically cast to an array again.
$json_table = (array) $json_table;
if ( isset( $json_table['data'] ) ) {
// JSON data contained a full export.
$table = $json_table;
} else {
// JSON data contained only the data of a table, but no options.
$table = array( 'data' => array() );
foreach ( $json_table as $row ) {
// Turn row into indexed arrays with numeric keys.
$row = array_values( (array) $row );
// Remove entries of multi-dimensional arrays.
foreach ( $row as &$cell ) {
if ( is_array( $cell ) ) {
$cell = '';
}
}
unset( $cell ); // Unset use-by-reference parameter of foreach loop.
$table['data'][] = $row;
}
}
$this->pad_array_to_max_cols( $table['data'] );
return $table;
}
/**
* Tries to import a table with the HTML format.
*
* @since 2.0.0
*
* @param string $data Data to import.
* @return array|WP_Error Table array on success, WP_Error if the file is not an HTML file.
*/
protected function _maybe_import_html( string $data ) /* : array|false */ {
TablePress::load_file( 'html-parser.class.php', 'libraries' );
$table = HTML_Parser::parse( $data );
// Check if the HTML code could be parsed. If not, this is probably not an HTML file.
if ( is_wp_error( $table ) ) {
return $table;
}
$this->pad_array_to_max_cols( $table['data'] );
return $table;
}
/**
* Tries to import a table via PHPSpreadsheet.
*
* @since 2.0.0
*
* @param File $file File to import.
* @return array|WP_Error Table array on success, WP_Error on error.
*/
protected function _import_phpspreadsheet( File $file ) /* : array|WP_Error */ {
// Rename the temporary file, as PHPSpreadsheet tries to infer the format from the file's extension.
if ( '' !== $file->extension ) {
$file_data = pathinfo( $file->location );
if ( ! isset( $file_data['extension'] ) || $file->extension !== $file_data['extension'] ) {
$temp_file = wp_tempnam();
$new_location = "{$temp_file}.{$file->extension}";
if ( $file->keep_file ) {
// Copy the file, as the original should be kept.
if ( copy( $file->location, $new_location ) ) {
$file->location = $new_location;
$file->keep_file = false; // Delete the newly created file after the import.
}
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
if ( rename( $file->location, $new_location ) ) {
$file->location = $new_location;
}
}
}
}
try {
// Treat all cell values as strings, except for formulas (due to recognition of quoted/escaped formulas like `'=A2`).
\TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( new \TablePress\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder() );
\TablePress\PhpOffice\PhpSpreadsheet\Cell\Cell::getValueBinder()->setFormulaConversion( false ); // @phpstan-ignore method.notFound
/*
* Try to detect a reader from the file extension and MIME type.
* Fall back to CSV if no reader could be determined.
*/
try {
$reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile( $file->location );
} catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception $exception ) {
$reader = \TablePress\PhpOffice\PhpSpreadsheet\IOFactory::createReader( 'Csv' );
// Change the file extension to .csv, so that \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::canRead() returns true.
$temp_file = wp_tempnam();
$new_location = "{$temp_file}.csv";
if ( $file->keep_file ) {
// Copy the file, as the original should be kept.
if ( copy( $file->location, $new_location ) ) {
$file->location = $new_location;
$file->keep_file = false; // Delete the newly created file after the import.
}
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
if ( rename( $file->location, $new_location ) ) {
$file->location = $new_location;
}
}
}
$class_name = get_class( $reader );
$class_type = explode( '\\', $class_name );
$detected_format = strtolower( array_pop( $class_type ) );
if ( 'csv' === $detected_format ) {
$reader->setInputEncoding( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Csv::GUESS_ENCODING ); // @phpstan-ignore method.notFound
// @phpstan-ignore method.notFound, smaller.alwaysFalse (PHPStan thinks that the Composer minimum version will always be fulfilled.)
$reader->setEscapeCharacter( ( PHP_VERSION_ID < 70400 ) ? "\x0" : '' ); // Disable the proprietary escape mechanism of PHP's fgetcsv() in PHP >= 7.4.
}
$reader->setIncludeCharts( false );
$reader->setReadEmptyCells( true );
// For non-Excel files, import only the data, but ignore formatting.
if ( ! in_array( $detected_format, array( 'xlsx', 'xls' ), true ) ) {
$reader->setReadDataOnly( true );
}
// For formats where it's supported, import only the first sheet.
if ( in_array( $detected_format, array( 'csv', 'html', 'slk' ), true ) ) {
$reader->setSheetIndex( 0 ); // @phpstan-ignore method.notFound
}
$spreadsheet = $reader->load( $file->location );
$worksheet = $spreadsheet->getActiveSheet();
$cell_collection = $worksheet->getCellCollection();
$comments = $worksheet->getComments();
$table = array(
'data' => array(),
);
$min_col = 'A';
$min_row = 1;
$max_col = $worksheet->getHighestColumn();
$max_row = $worksheet->getHighestRow();
// Adapted from \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::rangeToArray().
++$max_col; // Due to for-loop with characters for columns.
for ( $row = $min_row; $row <= $max_row; $row++ ) {
$row_data = array();
for ( $col = $min_col; $col !== $max_col; $col++ ) {
$cell_reference = $col . $row;
if ( ! $cell_collection->has( $cell_reference ) ) {
$row_data[] = '';
continue;
}
$cell = $cell_collection->get( $cell_reference );
$value = $cell->getValue();
if ( is_null( $value ) ) {
$row_data[] = '';
continue;
}
$cell_has_hyperlink = $worksheet->hyperlinkExists( $cell_reference ) && ! $worksheet->getHyperlink( $cell_reference )->isInternal();
if ( $value instanceof \TablePress\PhpOffice\PhpSpreadsheet\RichText\RichText ) {
$cell_data = $this->parse_rich_text( $value, $cell_has_hyperlink );
} else {
$cell_data = (string) $value;
}
// Apply data type formatting.
$style = $spreadsheet->getCellXfByIndex( $cell->getXfIndex() );
$format = $style->getNumberFormat()->getFormatCode() ?? \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL;
/*
* When cells in Excel files are formatted as "Text", quotation marks are removed, due to https://github.com/PHPOffice/PhpSpreadsheet/pull/3344.
* Setting the format to "General" seems to prevent that.
*/
if ( \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_TEXT === $format && ! is_numeric( $cell_data ) ) {
$format = \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL;
}
// Fix floating point precision issues with numbers in the "General" Excel .xlsx format.
if ( 'xlsx' === $detected_format && \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL === $format && is_numeric( $cell_data ) ) {
$cell_data = (string) (float) $cell_data; // Type-cast strings to float and back.
}
$cell_data = \TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat::toFormattedString(
$cell_data,
$format,
array( $this, 'format_color' ),
);
if ( strlen( $cell_data ) > 1 && '=' === $cell_data[0] ) {
if ( 'xlsx' === $detected_format && $style->getQuotePrefix() ) {
// Prepend a ' to quoted/escaped formulas (so that they are shown as text). This is currently not supported (at least) for the XLS format.
$cell_data = "'{$cell_data}";
} else {
// Bail early, to not add inline HTML styling around formulas, as they won't work anymore then.
$row_data[] = $cell_data;
continue;
}
}
$font = $style->getFont();
if ( $font->getSuperscript() ) {
$cell_data = "{$cell_data}";
}
if ( $font->getSubscript() ) {
$cell_data = "{$cell_data}";
}
if ( $font->getStrikethrough() ) {
$cell_data = "{$cell_data}";
}
if ( $font->getUnderline() !== \TablePress\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE && ! $cell_has_hyperlink ) {
$cell_data = "{$cell_data}";
}
if ( $font->getBold() ) {
$cell_data = "{$cell_data}";
}
if ( $font->getItalic() ) {
$cell_data = "{$cell_data}";
}
$color = $font->getColor()->getRGB();
if ( '' !== $color && '000000' !== $color && ! $cell_has_hyperlink ) {
// Don't add the span if the color is black, as that's the default, or if it's in a hyperlink.
$color_css = esc_attr( "color:#{$color};" );
$cell_data = "{$cell_data}";
}
// Convert Hyperlinks to HTML code.
if ( $cell_has_hyperlink ) {
$url = $worksheet->getHyperlink( $cell_reference )->getUrl();
if ( '' !== $url ) {
$title = $worksheet->getHyperlink( $cell_reference )->getTooltip();
if ( '' !== $title ) {
$title = ' title="' . esc_attr( $title ) . '"';
}
$url = esc_url( $url );
$cell_data = "{$cell_data}";
}
}
// Add comments.
if ( isset( $comments[ $cell_reference ] ) ) {
$sanitized_comment = esc_html( $worksheet->getComment( $cell_reference )->getText()->getPlainText() );
if ( '' !== $sanitized_comment ) {
$cell_data .= '
' . $sanitized_comment . '
';
}
}
$row_data[] = $cell_data;
}
$table['data'][] = $row_data;
}
// Convert merged cells to trigger words.
$merged_cells = $worksheet->getMergeCells();
foreach ( $merged_cells as $merged_cells_range ) {
$cells = explode( ':', $merged_cells_range );
$first_cell = \TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate::indexesFromString( $cells[0] );
$last_cell = \TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate::indexesFromString( $cells[1] );
for ( $row_idx = $first_cell[1]; $row_idx <= $last_cell[1]; $row_idx++ ) {
for ( $column_idx = $first_cell[0]; $column_idx <= $last_cell[0]; $column_idx++ ) {
if ( $row_idx === $first_cell[1] && $column_idx === $first_cell[0] ) {
continue; // Keep value of first cell.
} elseif ( $row_idx === $first_cell[1] && $column_idx > $first_cell[0] ) {
$table['data'][ $row_idx - 1 ][ $column_idx - 1 ] = '#colspan#';
} elseif ( $row_idx > $first_cell[1] && $column_idx === $first_cell[0] ) {
$table['data'][ $row_idx - 1 ][ $column_idx - 1 ] = '#rowspan#';
} else {
$table['data'][ $row_idx - 1 ][ $column_idx - 1 ] = '#span#';
}
}
}
}
// Save PHP memory.
$spreadsheet->disconnectWorksheets();
unset( $comments, $cell_collection, $worksheet, $spreadsheet );
return $table;
} catch ( \TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception | \TablePress\PhpOffice\PhpSpreadsheet\Exception $exception ) {
return new WP_Error( 'table_import_phpspreadsheet_failed', '', 'Exception: ' . $exception->getMessage() );
}
}
/**
* Parses PHPSpreadsheet RichText elements and converts formatting to HTML tags.
*
* @param \TablePress\PhpOffice\PhpSpreadsheet\RichText\RichText $value RichText element.
* @param bool $cell_has_hyperlink Whether the cell has a hyperlink.
* @return string Cell value with HTML formatting.
*/
protected function parse_rich_text( \TablePress\PhpOffice\PhpSpreadsheet\RichText\RichText $value, bool $cell_has_hyperlink ): string {
$cell_data = '';
$elements = $value->getRichTextElements();
foreach ( $elements as $element ) {
$element_data = $element->getText();
// Rich text start?
if ( $element instanceof \TablePress\PhpOffice\PhpSpreadsheet\RichText\Run ) {
$font = $element->getFont();
if ( $font->getSuperscript() ) {
$element_data = "{$element_data}";
}
if ( $font->getSubscript() ) {
$element_data = "{$element_data}";
}
if ( $font->getStrikethrough() ) {
$element_data = "{$element_data}";
}
if ( $font->getUnderline() !== \TablePress\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE ) {
$element_data = "{$element_data}";
}
if ( $font->getBold() ) {
$element_data = "{$element_data}";
}
if ( $font->getItalic() ) {
$element_data = "{$element_data}";
}
$color = $font->getColor()->getRGB();
if ( '' !== $color && '000000' !== $color && ! $cell_has_hyperlink ) {
// Don't add the span if the color is black, as that's the default, or if it's in a hyperlink.
$color_css = esc_attr( "color:#{$color};" );
$element_data = "{$element_data}";
}
}
$cell_data .= $element_data;
}
return $cell_data;
}
/**
* Adds color to formatted string as inline style, e.g. from conditional formatting.
*
* @param string $value Plain formatted value without color.
* @param string $format_code Format code.
* @return string Value with color format applied.
*/
public function format_color( string $value, string $format_code ): string {
// Color information, e.g. [Red] is always at the beginning of the format code.
$color = '';
if ( 1 === preg_match( '/^\\[[a-zA-Z]+\\]/', $format_code, $matches ) ) {
$color = str_replace( array( '[', ']' ), '', $matches[0] );
$color = strtolower( $color );
}
if ( '' !== $color ) {
$color = esc_attr( "color:{$color};" );
$value = "{$value}";
}
return $value;
}
} // class TablePress_Import_PHPSpreadsheet
class-controller.php 0000644 00000013703 15021224011 0010533 0 ustar 00 plugin_update_check();
}
/**
* Check if the plugin was updated and perform necessary actions, like updating the options.
*
* @since 1.0.0
*/
protected function plugin_update_check(): void {
// First activation or plugin update.
$current_plugin_options_db_version = TablePress::$model_options->get( 'plugin_options_db_version' );
if ( $current_plugin_options_db_version < TablePress::db_version ) {
// Allow more PHP execution time for update process.
if ( function_exists( 'set_time_limit' ) ) {
@set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
}
// Add TablePress capabilities to the WP_Roles objects, for new installations and all versions below 12.
if ( $current_plugin_options_db_version < 12 ) {
TablePress::$model_options->add_access_capabilities();
}
if ( 0 === TablePress::$model_options->get( 'first_activation' ) ) {
// Save initial set of plugin options, and time of first activation of the plugin, on first activation.
TablePress::$model_options->update( array(
'first_activation' => time(),
'plugin_options_db_version' => TablePress::db_version,
) );
} else {
// Update Plugin Options Options, if necessary.
TablePress::$model_options->merge_plugin_options_defaults();
$updated_options = array(
'plugin_options_db_version' => TablePress::db_version,
'prev_tablepress_version' => TablePress::$model_options->get( 'tablepress_version' ),
'tablepress_version' => TablePress::version,
'message_plugin_update' => true,
);
// If used, re-save "Custom CSS" to re-create all files (as TablePress Default CSS might have changed).
$custom_css = TablePress::$model_options->get( 'custom_css' );
if ( TablePress::$model_options->get( 'use_custom_css' ) && '' !== $custom_css ) {
/**
* Load WP file functions to provide filesystem access functions early.
*/
require_once ABSPATH . 'wp-admin/includes/file.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
/**
* Load WP admin template functions to provide `submit_button()` which is necessary for `request_filesystem_credentials()`.
*/
require_once ABSPATH . 'wp-admin/includes/template.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
$tablepress_css = TablePress::load_class( 'TablePress_CSS', 'class-css.php', 'classes' );
$custom_css_minified = TablePress::$model_options->get( 'custom_css_minified' );
// Update "Custom CSS" to be compatible with DataTables 2, introduced in TablePress 3.0.
if ( $current_plugin_options_db_version < 96 ) {
$old_custom_css = $custom_css;
$custom_css = TablePress::convert_datatables_api_data( $custom_css );
if ( $old_custom_css !== $custom_css ) {
$custom_css = $tablepress_css->sanitize_css( $custom_css );
$custom_css_minified = $tablepress_css->minify_css( $custom_css );
$updated_options['custom_css'] = $custom_css;
$updated_options['custom_css_minified'] = $custom_css_minified;
}
unset( $old_custom_css );
}
$result = $tablepress_css->save_custom_css_to_file( $custom_css, $custom_css_minified );
// If saving was successful, use "Custom CSS" file.
$updated_options['use_custom_css_file'] = $result;
// Increase the "Custom CSS" version number for cache busting.
if ( $result ) {
$updated_options['custom_css_version'] = TablePress::$model_options->get( 'custom_css_version' ) + 1;
}
}
TablePress::$model_options->update( $updated_options );
// Clear table caches.
TablePress::$model_table->invalidate_table_output_caches();
// Add mime type field to existing posts with the TablePress Custom Post Type, in TablePress 1.5.
if ( $current_plugin_options_db_version < 25 ) {
TablePress::$model_table->add_mime_type_to_posts();
}
// Add new access capabilities that were introduced in TablePress 2.3.2.
if ( $current_plugin_options_db_version < 77 ) {
TablePress::$model_options->add_access_capabilities_tp232();
}
// Update all tables' "Custom Commands" to be compatible with DataTables 2, introduced in TablePress 3.0.
if ( $current_plugin_options_db_version < 96 ) {
TablePress::$model_table->update_custom_commands_datatables_tp30();
}
}
}
// Maybe update the table scheme in each existing table, independently from updating the plugin options.
if ( TablePress::$model_options->get( 'table_scheme_db_version' ) < TablePress::table_scheme_version ) {
TablePress::$model_table->merge_table_options_defaults();
TablePress::$model_options->update( 'table_scheme_db_version', TablePress::table_scheme_version );
}
/*
* Update User Options, if necessary.
* User Options are not saved in DB until first change occurs.
*/
if ( is_user_logged_in() && TablePress::$model_options->get( 'user_options_db_version' ) < TablePress::db_version ) {
TablePress::$model_options->merge_user_options_defaults();
$updated_options = array(
'user_options_db_version' => TablePress::db_version,
'message_superseded_extensions' => true,
);
TablePress::$model_options->update( $updated_options );
}
}
} // class TablePress_Controller
class-model.php 0000644 00000001117 15021224011 0007444 0 ustar 00
*/
public array $export_formats = array();
/**
* Delimiters for the CSV export.
*
* @since 1.0.0
* @var array
*/
public array $csv_delimiters = array();
/**
* Whether ZIP archive support is available in the PHP installation on the server.
*
* @since 1.0.0
*/
public bool $zip_support_available = false;
/**
* Initialize the Export class.
*
* @since 1.0.0
*/
public function __construct() {
// Initiate here, because function call not possible outside a class method.
$this->export_formats = array(
'csv' => __( 'CSV - Character-Separated Values', 'tablepress' ),
'html' => __( 'HTML - Hypertext Markup Language', 'tablepress' ),
'json' => __( 'JSON - JavaScript Object Notation', 'tablepress' ),
);
$this->csv_delimiters = array(
';' => __( '; (semicolon)', 'tablepress' ),
',' => __( ', (comma)', 'tablepress' ),
'tab' => __( '\t (tabulator)', 'tablepress' ),
);
if ( class_exists( 'ZipArchive', false ) ) {
$this->zip_support_available = true;
}
}
/**
* Export a table.
*
* @since 1.0.0
*
* @param array $table Table to be exported.
* @param string $export_format Format for the export ('csv', 'html', 'json').
* @param string $csv_delimiter Delimiter for CSV export.
* @return string Exported table (only data for CSV and HTML, full tables (including options) for JSON).
*/
public function export_table( array $table, string $export_format, string $csv_delimiter ): string {
switch ( $export_format ) {
case 'csv':
$output = '';
if ( 'tab' === $csv_delimiter ) {
$csv_delimiter = "\t";
}
foreach ( $table['data'] as $row_idx => $row ) {
$csv_row = array();
foreach ( $row as $column_idx => $cell_content ) {
$csv_row[] = $this->csv_wrap_and_escape( $cell_content, $csv_delimiter );
}
$output .= implode( $csv_delimiter, $csv_row );
$output .= "\n";
}
break;
case 'html':
$num_rows = count( $table['data'] );
$last_row_idx = $num_rows - 1;
$thead = '';
$tfoot = '';
$tbody = array();
foreach ( $table['data'] as $row_idx => $row ) {
// Table head rows, but only if there's at least one additional row.
if ( $row_idx < $table['options']['table_head'] && $num_rows > $table['options']['table_head'] ) {
$thead = $this->html_render_row( $row, 'th' );
continue;
}
// Table foot rows, but only if there's at least one additional row.
if ( $row_idx > $last_row_idx - $table['options']['table_foot'] && $num_rows > $table['options']['table_foot'] ) {
$tfoot = $this->html_render_row( $row, 'th' );
continue;
}
// Neither first nor last row (with respective head/foot enabled), so render as body row.
$tbody[] = $this->html_render_row( $row, 'td' );
}
// , , and tags.
if ( ! empty( $thead ) ) {
$thead = "\t\n{$thead}\t\n";
}
if ( ! empty( $tfoot ) ) {
$tfoot = "\t\n{$tfoot}\t\n";
}
$tbody = "\t\n" . implode( '', $tbody ) . "\t\n";
$output = "
\n" . $thead . $tfoot . $tbody . "
\n";
break;
case 'json':
$output = wp_json_encode( $table, TABLEPRESS_JSON_OPTIONS );
if ( false === $output ) {
$output = '';
}
break;
default:
$output = '';
}
return $output;
}
/**
* Wrap and escape a cell for CSV export.
*
* @since 1.0.0
*
* @param string $cell_content Content of a cell.
* @param string $delimiter CSV delimiter character.
* @return string Wrapped string for CSV export.
*/
protected function csv_wrap_and_escape( string $cell_content, string $delimiter ): string {
// Return early if the cell is empty. No escaping or wrapping is needed then.
if ( '' === $cell_content ) {
return $cell_content;
}
// Escape potentially dangerous functions that could be used for CSV injection attacks in external spreadsheet software.
$active_content_triggers = array( '=', '+', '-', '@' );
if ( in_array( $cell_content[0], $active_content_triggers, true ) ) {
$functions_to_escape = array(
'cmd|',
'rundll32',
'DDE(',
'IMPORTXML(',
'IMPORTFEED(',
'IMPORTHTML(',
'IMPORTRANGE(',
'IMPORTDATA(',
'IMAGE(',
'HYPERLINK(',
'WEBSERVICE(',
);
foreach ( $functions_to_escape as $function ) {
if ( false !== stripos( $cell_content, $function ) ) {
$cell_content = "'" . $cell_content; // Prepend a ' to indicate that the cell format is a text string.
break;
}
}
}
// Escape CSV delimiter for RegExp (e.g. '|').
$delimiter = preg_quote( $delimiter, '#' );
if ( 1 === preg_match( '#' . $delimiter . '|"|\n|\r#i', $cell_content ) || str_starts_with( $cell_content, ' ' ) || str_ends_with( $cell_content, ' ' ) ) {
// Escape single " as double "".
$cell_content = str_replace( '"', '""', $cell_content );
// Wrap string in "".
$cell_content = '"' . $cell_content . '"';
}
return $cell_content;
}
/**
* Generate the HTML of a row.
*
* @since 1.0.0
*
* @param string[] $row Cells of the row to be rendered.
* @param string $tag HTML tag to use for the cells (td or th).
* @return string HTML code for the row.
*/
protected function html_render_row( array $row, string $tag ): string {
$output = "\t\t
\n";
return $output;
}
/**
* Wrap and escape a cell for HTML export.
*
* @since 1.0.0
*
* @param string $cell_content Content of a cell.
* @param int $column_idx Column index, or -1 if omitted. Unused, but defined to be able to use function as callback in array_walk().
* @param string $html_tag HTML tag that shall be used for the cell.
*/
protected function html_wrap_and_escape( string &$cell_content, int $column_idx, string $html_tag ): void {
/*
* Replace any & with & that is not already an encoded entity (from function htmlentities2 in WP 2.8).
* A complete htmlentities2() or htmlspecialchars() would encode tags, which we don't want.
*/
$cell_content = (string) preg_replace( '/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,4};)/', '&', $cell_content );
$cell_content = "\t\t\t<{$html_tag}>{$cell_content}{$html_tag}>\n";
}
} // class TablePress_Export
class-import-file.php 0000644 00000002736 15021224011 0010603 0 ustar 00 $properties Array of file properties.
*/
public function __construct( array $properties ) {
foreach ( $properties as $property => $value ) {
if ( property_exists( $this, $property ) ) {
$this->$property = $value;
}
}
}
} // class File
class-wp_user_option.php 0000644 00000005765 15021224011 0011435 0 ustar 00 ID, $this->option_name, false );
}
}
} // class TablePress_WP_User_Option
class-import-legacy.php 0000644 00000022707 15021224011 0011130 0 ustar 00 |false
*/
protected $imported_table = false;
/**
* Initialize the Import class.
*
* @since 1.0.0
*/
public function __construct() {
if ( class_exists( 'DOMDocument', false ) && function_exists( 'simplexml_import_dom' ) && function_exists( 'libxml_use_internal_errors' ) ) {
$this->html_import_support_available = true;
}
if ( $this->html_import_support_available ) {
$this->import_formats[] = 'html';
}
}
/**
* Import a table.
*
* @since 1.0.0
*
* @param string $format Import format.
* @param string $data Data to import.
* @return array|false Table array on success, false on error.
*/
public function import_table( string $format, string $data ) /* : array|false */ {
/**
* Filters the data that is to be imported.
*
* @since 1.8.1
*
* @param string $data Data to import.
* @param string $format Import format.
*/
$this->import_data = apply_filters( 'tablepress_import_table_data', $data, $format );
if ( ! in_array( $format, array( 'xlsx', 'xls' ), true ) ) {
$this->fix_table_encoding();
}
switch ( $format ) {
case 'csv':
$this->import_csv();
break;
case 'html':
$this->import_html();
break;
case 'json':
$this->import_json();
break;
case 'xlsx':
$this->import_xlsx();
break;
case 'xls':
$this->import_xls();
break;
default:
return false;
}
if ( false === $this->imported_table ) {
return false;
}
// Make sure that cells are stored as strings.
array_walk_recursive(
$this->imported_table['data'],
static function ( /* string|int|float|bool|null */ &$cell_content, int $col_idx ): void {
$cell_content = (string) $cell_content;
},
);
return $this->imported_table;
}
/**
* Import CSV data.
*
* @since 1.0.0
*/
protected function import_csv(): void {
$csv_parser = TablePress::load_class( 'CSV_Parser', 'csv-parser.class.php', 'libraries' );
$csv_parser->load_data( $this->import_data );
$delimiter = $csv_parser->find_delimiter();
$data = $csv_parser->parse( $delimiter );
$this->pad_array_to_max_cols( $data );
$this->normalize_line_endings( $data );
$this->imported_table = array( 'data' => $data );
}
/**
* Import HTML data.
*
* @since 1.0.0
*/
protected function import_html(): void {
if ( ! $this->html_import_support_available ) {
return;
}
TablePress::load_file( 'html-parser.class.php', 'libraries' );
$table = HTML_Parser::parse( $this->import_data );
if ( is_wp_error( $table ) ) {
$this->imported_table = false;
return;
}
$this->pad_array_to_max_cols( $table['data'] );
$this->normalize_line_endings( $table['data'] );
$this->imported_table = $table;
}
/**
* Import JSON data.
*
* @since 1.0.0
*/
protected function import_json(): void {
$json_table = json_decode( $this->import_data, true );
// Check if JSON could be decoded.
if ( is_null( $json_table ) ) {
$json_error = json_last_error_msg();
$output = '' . __( 'The imported file contains errors:', 'tablepress' ) . "
JSON error: {$json_error} ";
wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
}
// Specifically cast to an array again.
$json_table = (array) $json_table;
if ( isset( $json_table['data'] ) ) {
// JSON data contained a full export.
$table = $json_table;
} else {
// JSON data contained only the data of a table, but no options.
$table = array( 'data' => array() );
foreach ( $json_table as $row ) {
$table['data'][] = array_values( (array) $row );
}
}
$this->pad_array_to_max_cols( $table['data'] );
$this->imported_table = $table;
}
/**
* Import Microsoft Excel 97-2003 data.
*
* @since 1.1.0
*/
protected function import_xls(): void {
$excel_reader = TablePress::load_class( 'Spreadsheet_Excel_Reader', 'excel-reader.class.php', 'libraries', $this->import_data );
// Loop through Excel file and retrieve value and colspan/rowspan properties for each cell.
$sheet = 0; // 0 means first sheet of the Workbook
$table = array();
$num_rows = $excel_reader->rowcount( $sheet );
$num_columns = $excel_reader->colcount( $sheet );
for ( $row = 1; $row <= $num_rows; $row++ ) {
$table_row = array();
for ( $column = 1; $column <= $num_columns; $column++ ) {
$cell = array();
$cell['rowspan'] = $excel_reader->rowspan( $row, $column, $sheet );
$cell['colspan'] = $excel_reader->colspan( $row, $column, $sheet );
$cell['val'] = $excel_reader->val( $row, $column, $sheet );
$table_row[] = $cell;
}
$table[] = $table_row;
}
// Transform colspan/rowspan properties to TablePress equivalent (cell content).
foreach ( $table as $row_idx => $row ) {
foreach ( $row as $col_idx => $cell ) {
if ( 1 === $cell['rowspan'] && 1 === $cell['colspan'] ) {
continue;
}
if ( 1 < $cell['colspan'] ) {
for ( $i = 1; $i < $cell['colspan']; $i++ ) {
$table[ $row_idx ][ $col_idx + $i ]['val'] = '#colspan#';
}
}
if ( 1 < $cell['rowspan'] ) {
for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
$table[ $row_idx + $i ][ $col_idx ]['val'] = '#rowspan#';
}
}
if ( 1 < $cell['rowspan'] && 1 < $cell['colspan'] ) {
for ( $i = 1; $i < $cell['rowspan']; $i++ ) {
for ( $j = 1; $j < $cell['colspan']; $j++ ) {
$table[ $row_idx + $i ][ $col_idx + $j ]['val'] = '#span#';
}
}
}
}
}
// Flatten value property to two-dimensional array.
foreach ( $table as &$row ) {
foreach ( $row as &$cell ) {
$cell = $cell['val'];
}
unset( $cell ); // Unset use-by-reference parameter of foreach loop.
}
unset( $row ); // Unset use-by-reference parameter of foreach loop.
$this->imported_table = array( 'data' => $table );
}
/**
* Import Microsoft Excel 2007-2019 data.
*
* @since 1.1.0
*/
protected function import_xlsx(): void {
TablePress::load_file( 'simplexlsx.class.php', 'libraries' );
$xlsx_file = \Shuchkin\SimpleXLSX::parse( $this->import_data, true );
if ( ! $xlsx_file ) {
$output = '' . __( 'The imported file contains errors:', 'tablepress' ) . '
' . \Shuchkin\SimpleXLSX::parseError() . ' ';
wp_die( $output, 'Import Error', array( 'response' => 200, 'back_link' => true ) );
}
$this->imported_table = array( 'data' => $xlsx_file->rows() );
}
/**
* Fixes the encoding to UTF-8 for the entire string that is to be imported.
*
* @since 1.0.0
*
* @link http://stevephillips.me/blog/dealing-php-and-character-encoding
*/
protected function fix_table_encoding(): void {
// Check and remove possible UTF-8 Byte-Order Mark (BOM).
$bom = pack( 'CCC', 0xef, 0xbb, 0xbf );
if ( str_starts_with( $this->import_data, $bom ) ) {
$this->import_data = substr( $this->import_data, 3 );
// If data has a BOM, it's UTF-8, so further checks unnecessary.
return;
}
// Require the iconv() function for the following checks.
if ( ! function_exists( 'iconv' ) ) {
return;
}
// Check for possible UTF-16 BOMs ("little endian" and "big endian") and try to convert the data to UTF-8.
if ( str_starts_with( $this->import_data, "\xFF\xFE" ) || str_starts_with( $this->import_data, "\xFE\xFF" ) ) {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$data = @iconv( 'UTF-16', 'UTF-8', $this->import_data );
if ( false !== $data ) {
$this->import_data = $data;
return;
}
}
// Detect the character encoding and convert to UTF-8, if it's different.
if ( function_exists( 'mb_detect_encoding' ) ) {
$current_encoding = mb_detect_encoding( $this->import_data, 'ASCII, UTF-8, ISO-8859-1' );
if ( 'UTF-8' !== $current_encoding ) {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$data = @iconv( $current_encoding, 'UTF-8', $this->import_data ); // @phpstan-ignore argument.type
if ( false !== $data ) {
$this->import_data = $data;
return;
}
}
}
}
/**
* Replaces Windows line breaks \r\n with Unix line breaks \n.
*
* This function uses call by reference to save PHP memory on large arrays.
*
* @since 2.0.0
*
* @param array> $an_array Array in which Windows line breaks should be replaced by Unix line breaks.
*/
protected function normalize_line_endings( array &$an_array ): void {
array_walk_recursive(
$an_array,
static function ( string &$cell_content, int $col_idx ): void {
$cell_content = str_replace( "\r\n", "\n", $cell_content );
},
);
}
} // class TablePress_Import_Legacy
index.php 0000644 00000000034 15021224011 0006345 0 ustar 00
*/
public static array $modules = array();
/**
* Start-up TablePress (run on WordPress "init") and load the controller for the current state.
*
* @since 1.0.0
*/
public static function run(): void {
/**
* Fires before TablePress is loaded.
*
* The `tablepress_loaded` action hook might be a better choice in most situations, as TablePress options will then be available.
*
* @since 1.0.0
*/
do_action( 'tablepress_run' );
/**
* Filters the string that is used as the [table] Shortcode.
*
* @since 1.0.0
*
* @param string $shortcode The [table] Shortcode string.
*/
self::$shortcode = apply_filters( 'tablepress_table_shortcode', self::$shortcode );
/**
* Filters the string that is used as the [table-info] Shortcode.
*
* @since 1.0.0
*
* @param string $shortcode_info The [table-info] Shortcode string.
*/
self::$shortcode_info = apply_filters( 'tablepress_table_info_shortcode', self::$shortcode_info );
// Load modals for table and options, to be accessible from everywhere via `TablePress::$model_options` and `TablePress::$model_table`.
self::$model_options = self::load_model( 'options' );
self::$model_table = self::load_model( 'table' );
// Exit early, i.e. before a controller is loaded, if TablePress functionality is likely not needed.
$exit_early = false;
if ( ( isset( $_SERVER['SCRIPT_FILENAME'] ) && 'wp-login.php' === basename( $_SERVER['SCRIPT_FILENAME'] ) ) // Detect the WordPress Login screen.
|| ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
|| wp_doing_cron() ) {
$exit_early = true;
}
/**
* Filters whether TablePress should exit early, e.g. during wp-login.php, XML-RPC, and WP-Cron requests.
*
* @since 2.0.0
*
* @param bool $exit_early Whether TablePress should exit early.
*/
if ( apply_filters( 'tablepress_exit_early', $exit_early ) ) {
return;
}
if ( is_admin() ) {
$controller = 'admin';
if ( wp_doing_ajax() ) {
$controller = 'admin_ajax';
}
self::load_controller( $controller );
}
// Load the frontend controller in all scenarios, so that Shortcode render functions are always available.
self::$controller = self::load_controller( 'frontend' );
// Add filters and actions for the integration into the WP WXR exporter and importer.
add_action( 'wp_import_insert_post', array( TablePress::$model_table, 'add_table_id_on_wp_import' ), 10, 4 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
add_filter( 'wp_import_post_meta', array( TablePress::$model_table, 'prevent_table_id_post_meta_import_on_wp_import' ), 10, 3 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
add_filter( 'wxr_export_skip_postmeta', array( TablePress::$model_table, 'add_table_id_to_wp_export' ), 10, 3 ); // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
/**
* Fires after TablePress is loaded.
*
* The `tablepress_run` action hook can be used if code has to run before TablePress is loaded.
*
* @since 2.0.0
*/
do_action( 'tablepress_loaded' );
}
/**
* Load a file with require_once(), after running it through a filter.
*
* @since 1.0.0
*
* @param string $file Name of the PHP file.
* @param string $folder Name of the folder with the file.
*/
public static function load_file( string $file, string $folder ): void {
$full_path = TABLEPRESS_ABSPATH . $folder . '/' . $file;
/**
* Filters the full path of a file that shall be loaded.
*
* @since 1.0.0
*
* @param string $full_path Full path of the file that shall be loaded.
* @param string $file File name of the file that shall be loaded.
* @param string $folder Folder name of the file that shall be loaded.
*/
$full_path = apply_filters( 'tablepress_load_file_full_path', $full_path, $file, $folder );
if ( $full_path ) {
require_once $full_path;
}
}
/**
* Create a new instance of the $class_name, which is stored in $file in the $folder subfolder
* of the plugin's directory.
*
* @since 1.0.0
*
* @param string $class_name Name of the class.
* @param string $file Name of the PHP file with the class.
* @param string $folder Name of the folder with $class_name's $file.
* @param mixed[]|string|null $params Optional. Parameters that are passed to the constructor of $class_name.
* @return object Initialized instance of the class.
*/
public static function load_class( string $class_name, string $file, string $folder, /* ?array|string */ $params = null ): object {
/**
* Filters name of the class that shall be loaded.
*
* @since 1.0.0
*
* @param string $class_name Name of the class that shall be loaded.
*/
$class_name = apply_filters( 'tablepress_load_class_name', $class_name );
if ( ! class_exists( $class_name, false ) ) {
self::load_file( $file, $folder );
}
$the_class = new $class_name( $params );
return $the_class;
}
/**
* Create a new instance of the $model, which is stored in the "models" subfolder.
*
* @since 1.0.0
*
* @param string $model Name of the model.
* @return object Instance of the initialized model.
*/
public static function load_model( string $model ): object {
// Model Base Class.
self::load_file( 'class-model.php', 'classes' );
// Make first letter uppercase for a better looking naming pattern.
$ucmodel = ucfirst( $model );
$the_model = self::load_class( "TablePress_{$ucmodel}_Model", "model-{$model}.php", 'models' );
return $the_model;
}
/**
* Create a new instance of the $view, which is stored in the "views" subfolder, and set it up with $data.
*
* @since 1.0.0
*
* @param string $view Name of the view to load.
* @param array $data Optional. Parameters/PHP variables that shall be available to the view.
* @return object Instance of the initialized view, already set up, just needs to be rendered.
*/
public static function load_view( string $view, array $data = array() ): object {
// View Base Class.
self::load_file( 'class-view.php', 'classes' );
// Make first letter uppercase for a better looking naming pattern.
$ucview = ucfirst( $view );
$the_view = self::load_class( "TablePress_{$ucview}_View", "view-{$view}.php", 'views' );
$the_view->setup( $view, $data );
return $the_view;
}
/**
* Create a new instance of the $controller, which is stored in the "controllers" subfolder.
*
* @since 1.0.0
*
* @param string $controller Name of the controller.
* @return object Instance of the initialized controller.
*/
public static function load_controller( string $controller ): object {
// Controller Base Class.
self::load_file( 'class-controller.php', 'classes' );
// Make first letter uppercase for a better looking naming pattern.
$uccontroller = ucfirst( $controller );
$the_controller = self::load_class( "TablePress_{$uccontroller}_Controller", "controller-{$controller}.php", 'controllers' );
return $the_controller;
}
/**
* Generate the complete nonce string, from the nonce base, the action and an item, e.g. tablepress_delete_table_3.
*
* @since 1.0.0
*
* @param string $action Action for which the nonce is needed.
* @param string|false $item Optional. Item for which the action will be performed, like "table". false if no item should be used in the nonce.
* @return string The resulting nonce string.
*/
public static function nonce( string $action, /* string|false */ $item = false ): string {
$nonce = "tablepress_{$action}";
if ( $item ) {
$nonce .= "_{$item}";
}
return $nonce;
}
/**
* Check whether a nonce string is valid.
*
* @since 1.0.0
*
* @param string $action Action for which the nonce should be checked.
* @param string|false $item Optional. Item for which the action should be performed, like "table". false if no item should be used in the nonce.
* @param string $query_arg Optional. Name of the nonce query string argument in $_POST.
* @param bool $ajax Whether the nonce comes from an AJAX request.
*/
public static function check_nonce( string $action, /* string|false */ $item = false, string $query_arg = '_wpnonce', bool $ajax = false ): void {
$nonce_action = self::nonce( $action, $item );
if ( $ajax ) {
check_ajax_referer( $nonce_action, $query_arg );
} else {
check_admin_referer( $nonce_action, $query_arg );
}
}
/**
* Calculate the column index (number) of a column header string (example: A is 1, AA is 27, ...).
*
* For the opposite, @see number_to_letter().
*
* @since 1.0.0
*
* @param string $column Column string.
* @return int Column number, 1-based.
*/
public static function letter_to_number( string $column ): int {
$column = (string) preg_replace( '/[^A-Za-z]/', '', $column );
$column = strtoupper( $column );
$count = strlen( $column );
$number = 0;
for ( $i = 0; $i < $count; $i++ ) {
$number += ( ord( $column[ $count - 1 - $i ] ) - 64 ) * 26 ** $i;
}
return $number;
}
/**
* "Calculate" the column header string of a column index (example: 2 is B, AB is 28, ...).
*
* For the opposite, @see letter_to_number().
*
* @since 1.0.0
*
* @param int $number Column number, 1-based.
* @return string Column string.
*/
public static function number_to_letter( int $number ): string {
$column = '';
while ( $number > 0 ) {
$column = chr( 65 + ( ( $number - 1 ) % 26 ) ) . $column;
$number = intdiv( $number - 1, 26 );
}
return $column;
}
/**
* Get a nice looking date and time string from the mySQL format of datetime strings for output.
*
* @since 1.0.0
*
* @param string $datetime_string DateTime string, often in mySQL format..
* @param string $separator_or_format Optional. Separator between date and time, or format string.
* @return string Nice looking string with the date and time.
*/
public static function format_datetime( string $datetime_string, string $separator_or_format = ' ' ): string {
$timezone = wp_timezone();
$datetime = date_create( $datetime_string, $timezone );
if ( false === $datetime ) {
return $datetime_string;
}
$timestamp = $datetime->getTimestamp();
switch ( $separator_or_format ) {
case ' ':
case ' ':
case ' ':
case ' ':
$date = wp_date( get_option( 'date_format' ), $timestamp, $timezone );
$time = wp_date( get_option( 'time_format' ), $timestamp, $timezone );
$output = "{$date}{$separator_or_format}{$time}";
break;
default:
$output = (string) wp_date( $separator_or_format, $timestamp, $timezone );
break;
}
return $output;
}
/**
* Get the name from a WP user ID (used to store information on last editor of a table).
*
* @since 1.0.0
*
* @param int $user_id WP user ID.
* @return string Nickname of the WP user with the $user_id.
*/
public static function get_user_display_name( int $user_id ): string {
$user = get_userdata( $user_id );
return $user->display_name ?? sprintf( '%s', __( 'unknown', 'tablepress' ) );
}
/**
* Sanitizes a CSS class to ensure it only contains valid characters.
*
* Strips the string down to A-Z, a-z, 0-9, :, _, -.
* This is an extension to WP's `sanitize_html_class()`, to also allow `:` which are used in some CSS frameworks.
*
* @since 1.11.0
*
* @param string $css_class The CSS class name to be sanitized.
* @return string The sanitized CSS class.
*/
public static function sanitize_css_class( string $css_class ): string {
// Strip out any %-encoded octets.
$sanitized_css_class = (string) preg_replace( '|%[a-fA-F0-9][a-fA-F0-9]|', '', $css_class );
// Limit to A-Z, a-z, 0-9, ':', '_', and '-'.
$sanitized_css_class = (string) preg_replace( '/[^A-Za-z0-9:_-]/', '', $sanitized_css_class );
return $sanitized_css_class;
}
/**
* Extracts the top-level keys from a JavaScript object string.
*
* This function is used to extract the keys of the "Custom Commands" JavaScript object string, to check for overrides.
* It covers most cases, like normal object properties with and without quotes, shorthand properties, and shorthand methods,
* and also ignores single-line and multi-line comments.
* It does not cover all possible JavaScript syntax (like template literals, special characters, ...),
* but should be sufficient for the use case.
*
* @since 3.0.0
*
* @param string $js_object_string A JavaScript object as a string.
* @return string[] Array of top-level keys of the object.
*/
public static function extract_keys_from_js_object_string( string $js_object_string ): array {
$object_keys = array();
$length = strlen( $js_object_string );
$depth = 0;
$key_expected = true;
$in_quotes = false;
$quote_char = '';
$in_function_declaration = false;
$in_single_line_comment = false;
$in_multi_line_comment = false;
$object_key = '';
for ( $i = 0; $i < $length; $i++ ) {
$char = $js_object_string[ $i ];
// Skip parsing single-line comments.
if ( $in_single_line_comment ) {
if ( "\n" === $char ) {
$in_single_line_comment = false;
}
continue;
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
if ( '/' === $char && $i + 1 < $length && '/' === $js_object_string[ $i + 1 ] ) {
$in_single_line_comment = true;
++$i; // Skip the second '/'.
continue;
}
}
// Skip parsing multi-line comments.
if ( $in_multi_line_comment ) {
if ( '*' === $char && $i + 1 < $length && '/' === $js_object_string[ $i + 1 ] ) {
$in_multi_line_comment = false;
++$i; // Skip the '/' that ends the multi-line comment.
}
continue;
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
if ( '/' === $char && $i + 1 < $length && '*' === $js_object_string[ $i + 1 ] ) {
$in_multi_line_comment = true;
++$i; // Skip the '*'.
continue;
}
}
// Skip parsing while inside a quoted string.
if ( $in_quotes ) {
if ( $quote_char === $char ) {
$in_quotes = false;
}
continue;
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
if ( '"' === $char || "'" === $char ) {
$in_quotes = true;
$quote_char = $char;
continue;
}
}
/*
* Skip parsing while inside a `function abc( ... )` declaration string.
* The `$key_expected` check limits search the "function" string to object values.
* The check for the plain `f` reduces expensive `substr()` calls.
*/
if ( ! $key_expected ) {
if ( $in_function_declaration ) {
if ( ')' === $char ) {
$in_function_declaration = false;
}
continue;
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
if ( 'f' === $char && 'function' === substr( $js_object_string, $i, 8 ) ) {
$in_function_declaration = true;
$i += 7; // Skip the rest of the "function" string.
continue;
}
}
}
// Handle object depth, so that most parsing can be limited to the top level.
if ( '{' === $char || '[' === $char ) {
++$depth;
}
// Extract only keys at the top level.
if ( 1 === $depth ) {
if ( $key_expected ) {
if ( ':' === $char ) {
// Check for normal keys, with value after :.
// Go backwards to find the start of the key.
$j = $i - 1;
while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
--$j;
}
$key_end = $j; // Position of the last character of the key (potentially with quote).
if ( '"' === $js_object_string[ $j ] || "'" === $js_object_string[ $j ] ) {
// Quoted key.
$quote_char = $js_object_string[ $j ];
--$j;
while ( $j >= 0 && $quote_char !== $js_object_string[ $j ] ) {
--$j;
}
$key_start = $j + 1;
} else {
// Unquoted key.
while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
--$j;
}
$key_start = $j + 1;
}
$object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
$object_key = trim( $object_key, "\"'" );
if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
$object_keys[] = $object_key;
}
$key_expected = false;
} elseif ( ( ',' === $char || '}' === $char ) ) { // The `}` case is for the last key.
// Check for shorthand properties (which must be unquoted).
// Go backwards to find the start of the shorthand key.
$j = $i - 1;
while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
--$j;
}
$key_end = $j; // Position of the last character of the key (without a quote).
while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
--$j;
}
$key_start = $j + 1;
$object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
$object_keys[] = $object_key;
}
} elseif ( '(' === $char ) {
// Detect shorthand method definitions.
// Go back to find the start of the method name.
$j = $i - 1;
while ( $j >= 0 && preg_match( '/\s/', $js_object_string[ $j ] ) ) {
--$j;
}
$key_end = $j;
while ( $j >= 0 && preg_match( '/[\w]/', $js_object_string[ $j ] ) ) {
--$j;
}
$key_start = $j + 1;
$object_key = substr( $js_object_string, $key_start, $key_end - $key_start + 1 );
if ( '' !== $object_key && ! in_array( $object_key, $object_keys, true ) ) {
$object_keys[] = $object_key;
}
}
}
// Reset the "key expected" flag after a comma or closing brace.
if ( ',' === $char || '}' === $char ) {
$key_expected = true;
}
}
// Handle object depth.
if ( '}' === $char || ']' === $char ) {
--$depth;
}
}
return $object_keys;
}
/**
* Converts old DataTables 1.x CSS classes and parameters to the DataTables 2 variants.
*
* This function is used to modernize "Custom CSS" and "Custom Commands" for compatibility with DataTables 2.x.
* It probably does not catch all possible cases.
*
* @since 3.0.0
*
* @param string $code Code that contains DataTables 1.x CSS classes and parameters.
* @return string Updated code with DataTables 2.x CSS classes and parameters.
*/
public static function convert_datatables_api_data( string $code ): string {
/**
* Mappings for DataTables 1.x CSS class or parameter to DataTables 2 variants.
* As this array is used in `strtr()`, it's pre-sorted for descending string length of the array keys.
*/
static $datatables_api_data_mappings = array(
// CSS classes.
'.tablepress thead .sorting:hover' => '.tablepress thead .dt-orderable-asc:hover,.tablepress thead .dt-orderable-desc:hover',
'.tablepress thead .sorting_desc' => '.tablepress thead .dt-ordering-desc',
'.dataTables_filter label input' => '.dt-container .dt-search input',
'.tablepress thead .sorting_asc' => '.tablepress thead .dt-ordering-asc',
'.dataTables_scrollFootInner' => '.dt-scroll-footInner',
'.dataTables_scrollHeadInner' => '.dt-scroll-headInner',
'.tablepress thead .sorting' => '.tablepress thead .dt-orderable-asc,.tablepress thead .dt-orderable-desc',
'.dataTables_processing' => '.dt-processing',
'.dataTables_scrollBody' => '.dt-scroll-body',
'.dataTables_scrollFoot' => '.dt-scroll-foot',
'.dataTables_scrollHead' => '.dt-scroll-head',
'.dataTables_paginate' => '.dt-paging',
'.tablepress .even td' => '.tablepress>:where(tbody.row-striping)>:nth-child(odd)>*',
'.dataTables_wrapper' => '.dt-container',
'.tablepress .odd td' => '.tablepress>:where(tbody.row-striping)>:nth-child(even)>*',
'.dataTables_filter' => '.dt-search',
'.dataTables_length' => '.dt-length',
'.dataTables_scroll' => '.dt-scroll',
'.dataTables_empty' => '.dt-empty',
'.dataTables_info' => '.dt-info',
'.paginate_button' => '.dt-paging-button',
// DataTables API functions.
'$.fn.dataTable.' => 'DataTable.',
);
$code = strtr( $code, $datatables_api_data_mappings );
// HTML ID mappings, which were removed.
if ( str_contains( $code, '#tablepress-' ) ) {
$code = (string) preg_replace(
array(
'/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_paginate/',
'/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_filter/',
'/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_length/',
'/#tablepress-([A-Za-z1-9_-]|[A-Za-z0-9_-]{2,})_info/',
),
array(
'#tablepress-$1_wrapper .dt-paging',
'#tablepress-$1_wrapper .dt-search',
'#tablepress-$1_wrapper .dt-length',
'#tablepress-$1_wrapper .dt-info',
),
$code,
);
}
return $code;
}
/**
* Retrieves all information of a WP_Error object as a string.
*
* @since 1.4.0
*
* @param WP_Error $wp_error A WP_Error object.
* @return string All error codes, messages, and data of the WP_Error.
*/
public static function get_wp_error_string( WP_Error $wp_error ): string {
$error_strings = array();
$error_codes = $wp_error->get_error_codes();
// Reverse order to get latest errors first.
$error_codes = array_reverse( $error_codes );
foreach ( $error_codes as $error_code ) {
$error_strings[ $error_code ] = $error_code;
$error_messages = $wp_error->get_error_messages( $error_code );
$error_messages = implode( ', ', $error_messages );
if ( ! empty( $error_messages ) ) {
$error_strings[ $error_code ] .= " ({$error_messages})";
}
$error_data = $wp_error->get_error_data( $error_code );
if ( is_string( $error_data ) ) {
$error_strings[ $error_code ] .= " [{$error_data}]";
} elseif ( is_array( $error_data ) ) {
foreach ( $error_data as $key => $value ) {
$error_data[ $key ] = "{$key}: {$value}";
}
$error_data = implode( ', ', $error_data );
$error_strings[ $error_code ] .= " [{$error_data}]";
}
}
return implode( ";\n", $error_strings );
}
/**
* Generate the action URL, to be used as a link within the plugin (e.g. in the submenu navigation or List of Tables).
*
* @since 1.0.0
*
* @param array $params Optional. Parameters to form the query string of the URL.
* @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
* @param string $target Optional. Target File, e.g. "admin-post.php" for POST requests.
* @return string The URL for the given parameters (already run through esc_url() with $add_nonce === true!).
*/
public static function url( array $params = array(), bool $add_nonce = false, string $target = '' ): string {
// Default action is "list", if no action given.
if ( ! isset( $params['action'] ) ) {
$params['action'] = 'list';
}
$nonce_action = $params['action'];
if ( '' !== $target ) {
$params['action'] = "tablepress_{$params['action']}";
} else {
$params['page'] = 'tablepress';
// Top-level parent page needs special treatment for better action strings.
if ( self::$controller->is_top_level_page ) {
$target = 'admin.php';
if ( ! in_array( $params['action'], array( 'list', 'edit' ), true ) ) {
$params['page'] = "tablepress_{$params['action']}";
}
if ( ! in_array( $params['action'], array( 'edit' ), true ) ) {
$params['action'] = false;
}
} else {
$target = self::$controller->parent_page;
}
}
// $default_params also determines the order of the values in the query string.
$default_params = array(
'page' => false,
'action' => false,
'item' => false,
);
$params = array_merge( $default_params, $params );
$url = add_query_arg( $params, admin_url( $target ) );
if ( $add_nonce ) {
$url = wp_nonce_url( $url, self::nonce( $nonce_action, $params['item'] ) ); // wp_nonce_url() does esc_html().
}
return $url;
}
/**
* Create a redirect URL from the $target_parameters and redirect the user.
*
* @since 1.0.0
*
* @param array $params Optional. Parameters from which the target URL is constructed.
* @param bool $add_nonce Optional. Whether the URL shall be nonced by WordPress.
*/
public static function redirect( array $params = array(), bool $add_nonce = false ): void {
$redirect = self::url( $params );
if ( $add_nonce ) {
if ( ! isset( $params['item'] ) ) {
$params['item'] = false;
}
// Don't use wp_nonce_url(), as that uses esc_html().
$redirect = add_query_arg( '_wpnonce', wp_create_nonce( self::nonce( $params['action'], $params['item'] ) ), $redirect );
}
wp_redirect( $redirect );
exit;
}
/**
* Determines the editor that the site uses, so that certain text and input fields referring to Shortcodes can be displayed or not.
*
* @since 3.1.0
*
* @return string The editor that the site uses, either "block", "elementor", or "other".
*/
public static function site_used_editor(): string {
if ( is_plugin_active( 'elementor/elementor.php' ) ) {
return 'elementor';
}
// Checking for Elementor is not needed anymore in this condition.
$site_uses_block_editor = use_block_editor_for_post_type( 'post' )
&& ! is_plugin_active( 'classic-editor/classic-editor.php' )
&& ! is_plugin_active( 'classic-editor-addon/classic-editor-addon.php' )
&& ! is_plugin_active( 'siteorigin-panels/siteorigin-panels.php' )
&& ! is_plugin_active( 'beaver-builder-lite-version/fl-builder.php' );
/**
* Filters the outcome of the check whether the site uses the block editor.
*
* This can be used when certain conditions (e.g. new site builders) are not (yet) accounted for.
*
* @since 2.0.1
*
* @param bool $site_uses_block_editor True if the site uses the block editor, false otherwise.
*/
$site_uses_block_editor = (bool) apply_filters( 'tablepress_site_uses_block_editor', $site_uses_block_editor );
if ( $site_uses_block_editor ) {
return 'block';
}
return 'other';
}
/**
* Initializes the list of TablePress premium modules.
*
* @since 2.1.0
*/
public static function init_modules(): void {
if ( ! empty( self::$modules ) ) {
return;
}
self::$modules = array(
'advanced-access-rights' => array(
'name' => __( 'Advanced Access Rights', 'tablepress' ),
'description' => __( 'Restrict access to individual tables for individual users.', 'tablepress' ),
'category' => 'backend',
'class' => 'TablePress_Module_Advanced_Access_Rights',
'incompatible_classes' => array( 'TablePress_Advanced_Access_Rights_Controller' ),
'minimum_plan' => 'max',
'default_active' => false,
),
'automatic-periodic-table-import' => array(
'name' => __( 'Automatic Periodic Table Import', 'tablepress' ),
'description' => __( 'Periodically update tables from a configured import source.', 'tablepress' ),
'category' => 'backend',
'class' => 'TablePress_Module_Automatic_Periodic_Table_Import',
'incompatible_classes' => array( 'TablePress_Table_Auto_Update' ),
'minimum_plan' => 'max',
'default_active' => true,
),
'automatic-table-export' => array(
'name' => __( 'Automatic Table Export', 'tablepress' ),
'description' => __( 'Export and save tables to files on the server after they were modified.', 'tablepress' ),
'category' => 'backend',
'class' => 'TablePress_Module_Automatic_Table_Export',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => false,
),
'cell-highlighting' => array(
'name' => __( 'Cell Highlighting', 'tablepress' ),
'description' => __( 'Add CSS classes to cells for highlighting based on their content.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_Cell_Highlighting',
'incompatible_classes' => array( 'TablePress_Cell_Highlighting' ),
'minimum_plan' => 'pro',
'default_active' => false,
),
'column-order' => array(
'name' => __( 'Column Order', 'tablepress' ),
'description' => __( 'Order the columns in different ways when a table is shown.', 'tablepress' ),
'category' => 'data-management',
'class' => 'TablePress_Module_Column_Order',
'incompatible_classes' => array( 'TablePress_Column_Order' ),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-advanced-loading' => array(
'name' => __( 'Advanced Loading', 'tablepress' ),
'description' => __( 'Load the table data from a JSON array for faster loading.', 'tablepress' ),
'category' => 'backend',
'class' => 'TablePress_Module_DataTables_Advanced_Loading',
'incompatible_classes' => array( 'TablePress_DataTables_Advanced_Loading' ),
'minimum_plan' => 'max',
'default_active' => false,
),
'datatables-alphabetsearch' => array(
'name' => __( 'Alphabet Search', 'tablepress' ),
'description' => __( 'Show Alphabet buttons above the table to filter rows by their first letter.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_Alphabetsearch',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-auto-filter' => array(
'name' => __( 'Automatic Filter', 'tablepress' ),
'description' => __( 'Pre-filter a table when it is shown.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_Auto_Filter',
'incompatible_classes' => array( 'TablePress_DataTables_Auto_Filter' ),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-buttons' => array(
'name' => __( 'User Action Buttons', 'tablepress' ),
'description' => __( 'Add buttons for downloading, copying, printing, and changing column visibility of tables.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_DataTables_Buttons',
'incompatible_classes' => array( 'TablePress_DataTables_Buttons' ),
'minimum_plan' => 'pro',
'default_active' => true,
),
'datatables-columnfilterwidgets' => array(
'name' => __( 'Column Filter Dropdowns', 'tablepress' ),
'description' => __( 'Add a search dropdown for each column above the table.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_ColumnFilterWidgets',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => true,
),
'datatables-column-filter' => array(
'name' => __( 'Individual Column Filtering', 'tablepress' ),
'description' => __( 'Add a search field for each column to the table head or foot row.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_Column_Filter',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-counter-column' => array(
'name' => __( 'Index Column', 'tablepress' ),
'description' => __( 'Make the first column an index or counter column with the row position.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_DataTables_Counter_Column',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-fixedheader-fixedcolumns' => array(
'name' => __( 'Fixed Rows and Columns', 'tablepress' ),
'description' => __( 'Fix the header and footer row and the first and last column when scrolling the table.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_DataTables_FixedHeader_FixedColumns',
'incompatible_classes' => array(
'TablePress_DataTables_FixedHeader',
'TablePress_DataTables_FixedColumns',
),
'minimum_plan' => 'pro',
'default_active' => true,
),
'datatables-layout' => array(
'name' => __( 'Table Layout', 'tablepress' ),
'description' => __( 'Customize the layout and position of features around a table.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_DataTables_Layout',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => true,
),
'datatables-fuzzysearch' => array(
'name' => __( 'Fuzzy Search', 'tablepress' ),
'description' => __( 'Let the search account for spelling mistakes and typos and find similar matches.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_FuzzySearch',
'incompatible_classes' => array(),
'minimum_plan' => 'max',
'default_active' => false,
),
'datatables-inverted-filter' => array(
'name' => __( 'Inverted Filtering', 'tablepress' ),
'description' => __( 'Turn the filtering into a search and hide the table if no search term is entered.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_Inverted_Filter',
'incompatible_classes' => array(),
'minimum_plan' => 'max',
'default_active' => false,
),
'datatables-pagination' => array(
'name' => __( 'Advanced Pagination Settings', 'tablepress' ),
'description' => __( 'Customize the pagination settings of the table.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_DataTables_Pagination',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-rowgroup' => array(
'name' => __( 'Row Grouping', 'tablepress' ),
'description' => __( 'Group table rows by a common keyword, category, or title.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_DataTables_RowGroup',
'incompatible_classes' => array( 'TablePress_DataTables_RowGroup' ),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-searchbuilder' => array(
'name' => __( 'Custom Search Builder', 'tablepress' ),
'description' => __( 'Show a search builder interface for filtering from groups and using conditions.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_SearchBuilder',
'incompatible_classes' => array(),
'minimum_plan' => 'max',
'default_active' => false,
),
'datatables-searchhighlight' => array(
'name' => __( 'Search Highlighting', 'tablepress' ),
'description' => __( 'Highlight found search terms in the table.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_SearchHighlight',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-searchpanes' => array(
'name' => __( 'Search Panes', 'tablepress' ),
'description' => __( 'Show panes for filtering the columns.', 'tablepress' ),
'category' => 'search-filter',
'class' => 'TablePress_Module_DataTables_SearchPanes',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => false,
),
'datatables-serverside-processing' => array(
'name' => __( 'Server-side Processing', 'tablepress' ),
'description' => __( 'Process sorting, filtering, and pagination on the server for faster loading of large tables.', 'tablepress' ),
'category' => 'backend',
'class' => 'TablePress_Module_DataTables_ServerSide_Processing',
'incompatible_classes' => array(),
'minimum_plan' => 'max',
'default_active' => true,
),
'default-style-customizer' => array(
'name' => __( 'Default Style Customizer', 'tablepress' ),
'description' => __( 'Change the default styling of your tables in the visual style customizer.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_Default_Style_Customizer',
'incompatible_classes' => array(),
'minimum_plan' => 'pro',
'default_active' => true,
),
'email-notifications' => array(
'name' => __( 'Email Notifications', 'tablepress' ),
'description' => __( 'Get email notifications when certain actions are performed on tables.', 'tablepress' ),
'category' => 'backend',
'class' => 'TablePress_Module_Email_Notifications',
'incompatible_classes' => array(),
'minimum_plan' => 'max',
'default_active' => false,
),
'responsive-tables' => array(
'name' => __( 'Responsive Tables', 'tablepress' ),
'description' => __( 'Make your tables look good on different screen sizes.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_Responsive_Tables',
'incompatible_classes' => array( 'TablePress_Responsive_Tables' ),
'minimum_plan' => 'pro',
'default_active' => true,
),
'rest-api' => array(
'name' => __( 'REST API', 'tablepress' ),
'description' => __( 'Read table data via the WordPress REST API, e.g. in external apps.', 'tablepress' ),
'category' => 'backend',
'class' => 'TablePress_Module_REST_API',
'incompatible_classes' => array( 'TablePress_REST_API_Controller' ),
'minimum_plan' => 'max',
'default_active' => false,
),
'row-filtering' => array(
'name' => __( 'Row Filtering', 'tablepress' ),
'description' => __( 'Show only table rows that contain defined keywords.', 'tablepress' ),
'category' => 'data-management',
'class' => 'TablePress_Module_Row_Filtering',
'incompatible_classes' => array( 'TablePress_Row_Filter' ),
'minimum_plan' => 'pro',
'default_active' => true,
),
'row-highlighting' => array(
'name' => __( 'Row Highlighting', 'tablepress' ),
'description' => __( 'Add CSS classes to rows for highlighting based on their content.', 'tablepress' ),
'category' => 'frontend',
'class' => 'TablePress_Module_Row_Highlighting',
'incompatible_classes' => array( 'TablePress_Row_Highlighting' ),
'minimum_plan' => 'pro',
'default_active' => false,
),
'row-order' => array(
'name' => __( 'Row Order', 'tablepress' ),
'description' => __( 'Order the rows in different ways when a table is shown.', 'tablepress' ),
'category' => 'data-management',
'class' => 'TablePress_Module_Row_Order',
'incompatible_classes' => array( 'TablePress_Row_Order' ),
'minimum_plan' => 'pro',
'default_active' => false,
),
);
}
} // class TablePress
class-view.php 0000644 00000051142 15021224011 0007321 0 ustar 00
*/
protected array $data = array();
/**
* Number of screen columns for post boxes.
*
* @since 1.0.0
*/
protected int $screen_columns = 0;
/**
* User action for this screen.
*
* @since 1.0.0
*/
protected string $action = '';
/**
* Instance of the Admin Page Helper Class, with necessary functions.
*
* @since 1.0.0
*/
protected \TablePress_Admin_Page $admin_page;
/**
* List of text boxes (similar to post boxes, but just with text and without extra functionality).
*
* @since 1.0.0
* @var array>>
*/
protected array $textboxes = array();
/**
* List of messages that are to be displayed as boxes below the page title.
*
* @since 1.0.0
* @var string[]
*/
protected array $header_messages = array();
/**
* Whether there are post boxes registered for this screen,
* is automatically set to true, when a meta box is added.
*
* @since 1.0.0
*/
protected bool $has_meta_boxes = false;
/**
* List of WP feature pointers for this view.
*
* @since 1.0.0
* @var string[]
*/
protected array $wp_pointers = array();
/**
* Initializes the View class, by setting the correct screen columns and adding help texts.
*
* @since 1.0.0
*/
public function __construct() {
$screen = get_current_screen();
if ( 0 !== $this->screen_columns ) {
$screen->add_option( 'layout_columns', array( 'max' => $this->screen_columns ) ); // @phpstan-ignore method.nonObject
}
// Enable two column layout.
add_filter( "get_user_option_screen_layout_{$screen->id}", array( $this, 'set_current_screen_layout_columns' ) ); // @phpstan-ignore property.nonObject
$common_content = '
' . sprintf( __( 'More information about TablePress can be found on the plugin website or on its page in the WordPress Plugin Directory.', 'tablepress' ), 'https://tablepress.org/', 'https://wordpress.org/plugins/tablepress/' ) . '
';
$common_content .= '
' . sprintf( __( 'For technical information, please see the Documentation.', 'tablepress' ), 'https://tablepress.org/documentation/' ) . ' ' . sprintf( __( 'Common questions are answered in the FAQ.', 'tablepress' ), 'https://tablepress.org/faq/' ) . '
';
if ( tb_tp_fs()->is_free_plan() ) {
$common_content .= '
'
. sprintf( __( 'Support is provided through the WordPress Support Forums.', 'tablepress' ), 'https://tablepress.org/support/', 'https://wordpress.org/tags/tablepress' )
. ' '
. sprintf( __( 'Before asking for support, please carefully read the Frequently Asked Questions, where you will find answers to the most common questions, and search through the forums.', 'tablepress' ), 'https://tablepress.org/faq/' )
. '
';
$common_content .= '
' . sprintf( __( 'More great features for you and your site’s visitors and priority email support are available with a Premium license plan of TablePress. Go check them out!', 'tablepress' ), 'https://tablepress.org/premium/?utm_source=plugin&utm_medium=textlink&utm_content=help-tab' ) . '
';
}
$screen->add_help_tab( array( // @phpstan-ignore method.nonObject
'id' => 'tablepress-help', // This should be unique for the screen.
'title' => __( 'TablePress Help', 'tablepress' ),
'content' => '
' . $this->help_tab_content() . '
' . $common_content,
) );
// "Sidebar" in the help tab.
$screen->set_help_sidebar( // @phpstan-ignore method.nonObject
'
' . __( 'For more information:', 'tablepress' ) . '
'
);
}
/**
* Changes the value of the user option "screen_layout_{$screen->id}" through a filter.
*
* @since 1.0.0
*
* @param int|false $result Current value of the user option.
* @return int New value for the user option.
*/
public function set_current_screen_layout_columns( /* int|false */ $result ): int {
if ( false === $result ) {
// The user option does not yet exist.
$result = $this->screen_columns;
} elseif ( $result > $this->screen_columns ) {
// The value of the user option is bigger than what is possible on this screen (e.g. because the number of columns was reduced in an update).
$result = $this->screen_columns;
}
return $result;
}
/**
* Sets up the view with data and do things that are necessary for all views.
*
* @since 1.0.0
*
* @param string $action Action for this view.
* @param array $data Data for this view.
*/
public function setup( /* string */ $action, array $data ) /* : void */ {
// Don't use type hints (except array $data) in method declaration, as the method is extended in some TablePress Extensions which are no longer updated.
$this->action = $action;
$this->data = $data;
// Set page title.
$GLOBALS['title'] = sprintf( __( '%1$s ‹ %2$s', 'tablepress' ), $this->data['view_actions'][ $this->action ]['page_title'], 'TablePress' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
// Admin page helpers, like script/style loading, could be moved to view.
$this->admin_page = TablePress::load_class( 'TablePress_Admin_Page', 'class-admin-page-helper.php', 'classes' );
$this->admin_page->enqueue_style( 'common', array( 'wp-components' ) );
// RTL styles for the admin interface.
if ( is_rtl() ) {
$this->admin_page->enqueue_style( 'common-rtl', array( 'tablepress-common' ) );
}
$this->admin_page->enqueue_script( 'common', array( 'jquery-core', 'postbox' ) );
$this->admin_page->add_admin_footer_text();
// Initialize WP feature pointers for TablePress.
$this->_init_wp_pointers();
// Necessary fields for all views.
$this->add_text_box( 'default_nonce_fields', array( $this, 'default_nonce_fields' ), 'header', false );
$this->add_text_box( 'action_nonce_field', array( $this, 'action_nonce_field' ), 'header', false );
$this->add_text_box( 'action_field', array( $this, 'action_field' ), 'header', false );
}
/**
* Registers a header message for the view.
*
* @since 1.0.0
*
* @param string $text Text for the header message.
* @param string $css_class Optional. Additional CSS class for the header message.
* @param string $title Optional. Text for the header title.
*/
protected function add_header_message( string $text, string $css_class = 'is-success', string $title = '' ): void {
if ( ! str_contains( $css_class, 'not-dismissible' ) ) {
$css_class .= ' is-dismissible';
}
if ( '' !== $title ) {
$title = "
{$title}
";
}
// Wrap the message text in HTML
tags if it does not already start with one (potentially with attributes), indicating custom message HTML.
if ( '' !== $text && ! str_starts_with( $text, '
{$text}
";
}
$this->header_messages[] = "
{$title}{$text}
\n";
}
/**
* Processes header action messages, i.e. check if a message should be added to the page.
*
* @since 1.0.0
*
* @param array $action_messages Action messages for the screen.
*/
protected function process_action_messages( array $action_messages ): void {
if ( $this->data['message'] && isset( $action_messages[ $this->data['message'] ] ) ) {
$class = ( str_starts_with( $this->data['message'], 'error' ) ) ? 'is-error' : 'is-success';
if ( '' !== $this->data['error_details'] ) {
$this->data['error_details'] = '
' . sprintf( __( 'Error code: %s', 'tablepress' ), '' . esc_html( $this->data['error_details'] ) . '' );
}
$this->add_header_message( "{$action_messages[ $this->data['message'] ]}{$this->data['error_details']}", $class );
}
}
/**
* Registers a text box for the view.
*
* @since 1.0.0
*
* @param string $id Unique HTML ID for the text box container (only visible with $wrap = true).
* @param callable $callback Callback that prints the contents of the text box.
* @param string $context Optional. Context/position of the text box (normal, side, additional, header, submit).
* @param bool $wrap Whether the content of the text box shall be wrapped in a
container.
*/
protected function add_text_box( string $id, callable $callback, string $context = 'normal', bool $wrap = false ): void {
if ( ! isset( $this->textboxes[ $context ] ) ) {
$this->textboxes[ $context ] = array();
}
$long_id = "tablepress_{$this->action}-{$id}";
$this->textboxes[ $context ][ $id ] = array(
'id' => $long_id,
'callback' => $callback,
'context' => $context,
'wrap' => $wrap,
);
}
/**
* Registers a post meta box for the view, that is drag/droppable with WordPress functionality.
*
* @since 1.0.0
*
* @param string $id Unique ID for the meta box.
* @param string $title Title for the meta box.
* @param callable $callback Callback that prints the contents of the post meta box.
* @param 'normal'|'side'|'additional' $context Optional. Context/position of the post meta box (normal, side, additional).
* @param 'core'|'default'|'high'|'low' $priority Optional. Order of the post meta box for the $context position (high, default, low).
* @param mixed[]|null $callback_args Optional. Additional data for the callback function (e.g. useful when in different class).
*/
protected function add_meta_box( string $id, string $title, callable $callback, string $context = 'normal', string $priority = 'default', ?array $callback_args = null ): void {
$this->has_meta_boxes = true;
add_meta_box( "tablepress_{$this->action}-{$id}", $title, $callback, null, $context, $priority, $callback_args );
}
/**
* Renders all text boxes for the given context.
*
* @since 1.0.0
*
* @param string $context Context (normal, side, additional, header, submit) for which registered text boxes shall be rendered.
*/
protected function do_text_boxes( string $context ): void {
if ( empty( $this->textboxes[ $context ] ) ) {
return;
}
foreach ( $this->textboxes[ $context ] as $box ) {
if ( $box['wrap'] ) {
echo "
\n";
}
}
}
/**
* Renders all post meta boxes for the given context, if there are post meta boxes.
*
* @since 1.0.0
*
* @param string $context Context (normal, side, additional) for which registered post meta boxes shall be rendered.
*/
protected function do_meta_boxes( string $context ): void {
if ( $this->has_meta_boxes ) {
do_meta_boxes( get_current_screen(), $context, $this->data ); // @phpstan-ignore argument.type
}
}
/**
* Prints hidden fields with nonces for post meta box AJAX handling, if there are post meta boxes on the screen.
*
* The check is possible as this function is executed after post meta boxes have to be registered.
*
* @since 1.0.0
*
* @param array $data Data for this screen.
* @param array $box Information about the text box.
*/
protected function default_nonce_fields( array $data, array $box ): void {
if ( ! $this->has_meta_boxes ) {
return;
}
wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
echo "\n";
wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false );
echo "\n";
}
/**
* Prints hidden field with a nonce for the screen's action, to be transmitted in HTTP requests.
*
* @since 1.0.0
*
* @param array $data Data for this screen.
* @param array $box Information about the text box.
*/
protected function action_nonce_field( array $data, array $box ): void {
wp_nonce_field( TablePress::nonce( $this->action ) );
echo "\n";
}
/**
* Prints hidden field with the screen action.
*
* @since 1.0.0
*
* @param array $data Data for this screen.
* @param array $box Information about the text box.
*/
protected function action_field( array $data, array $box ): void {
echo "action}\">\n";
}
/**
* Renders the current view.
*
* @since 1.0.0
*/
public function render(): void {
?>
$data Data for this screen.
* @param array $box Information about the text box.
*/
public function textbox_no_javascript( array $data, array $box ): void {
?>
the instructions on how to enable JavaScript in your browser.', 'tablepress' ); ?>
'list' ) ) ) . '">' . __( 'Back to the List of Tables', 'tablepress' ) . ''; ?>
$data Data for this screen.
* @param array $box Information about the text box.
*/
protected function textbox_submit_button( array $data, array $box ): void {
?>
$data Information about the text box.
*/
protected function print_script_data_json( string $variable, array $data ): void {
echo "\n";
}
/**
* Returns the content for the help tab for this screen.
*
* Has to be implemented for every view that is visible in the WP Dashboard!
*
* @since 1.0.0
*
* @return string Help tab content for the view.
*/
protected function help_tab_content(): string {
// Has to be implemented for every view that is visible in the WP Dashboard!
return '';
}
/**
* Initializes the WP feature pointers for TablePress.
*
* @since 1.0.0
*/
protected function _init_wp_pointers(): void {
// Check if there are WP pointers for this view.
if ( empty( $this->wp_pointers ) ) {
return;
}
// Get dismissed pointers.
$dismissed = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
$pointers_on_page = false;
foreach ( array_diff( $this->wp_pointers, $dismissed ) as $pointer ) {
// Bind pointer print function.
add_action( "admin_footer-{$GLOBALS['hook_suffix']}", array( $this, 'wp_pointer_' . $pointer ) ); // @phpstan-ignore argument.type
$pointers_on_page = true;
}
if ( $pointers_on_page ) {
wp_enqueue_style( 'wp-pointer' );
wp_enqueue_script( 'wp-pointer' );
}
}
} // class TablePress_View
class-import.php 0000644 00000076103 15021224011 0007665 0 ustar 00
*/
protected array $import_config = array();
/**
* Whether ZIP archive support is available (which it always is, as PclZip is used as a fallback).
*
* @since 1.0.0
* @deprecated 2.3.0 ZIP support is now always available, either through `ZipArchive` or through `PclZip`.
*/
public bool $zip_support_available = true;
/**
* List of table names/IDs for use when replacing/appending existing tables (except for the JSON format).
*
* @since 2.0.0
* @var array
*/
protected array $table_names_ids = array();
/**
* Runs the import process for a given import configuration.
*
* @since 2.0.0
*
* @param array $import_config Import configuration.
* @return array{tables: array>, errors: File[]}|WP_Error List of imported tables on success, WP_Error on failure.
*/
public function run( array $import_config ) /* : array|WP_Error */ {
// Unziping can use a lot of memory and execution time, but not this much hopefully.
wp_raise_memory_limit( 'admin' );
if ( function_exists( 'set_time_limit' ) ) {
@set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
}
$this->import_config = $import_config;
$import_files = $this->get_files_to_import();
if ( is_wp_error( $import_files ) ) {
return $import_files;
}
$import_files = $this->convert_zip_files( $import_files );
if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) ) {
$this->table_names_ids = $this->get_list_of_table_names();
}
return $this->import_files( $import_files );
}
/**
* Extracts the files that shall be imported from the import configuration.
*
* @since 2.0.0
*
* @return File[]|WP_Error Array of files that shall be imported or WP_Error on failure.
*/
protected function get_files_to_import() /* : array|WP_Error */ {
$import_files = array();
switch ( $this->import_config['source'] ) {
case 'file-upload':
foreach ( $this->import_config['file-upload']['error'] as $key => $error ) {
$file = new File( array(
'location' => $this->import_config['file-upload']['tmp_name'][ $key ],
'name' => $this->import_config['file-upload']['name'][ $key ],
) );
if ( UPLOAD_ERR_OK !== $error ) {
@unlink( $this->import_config['file-upload']['tmp_name'][ $key ] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$file->error = new WP_Error( 'table_import_file-upload_error', '', $error );
}
$import_files[] = $file;
}
break;
case 'url':
$host = wp_parse_url( $this->import_config['url'], PHP_URL_HOST );
if ( empty( $host ) ) {
return new WP_Error( 'table_import_url_host_invalid', '', $this->import_config['url'] );
}
// Check the IP address of the host against a blocklist of hosts which should not be accessible, e.g. for security considerations.
$ip = gethostbyname( $host ); // If no IP address can be found, this will return the host name, which will then be checked against the blocklist.
$blocked_ips = array(
'169.254.169.254', // Meta-data API for various cloud providers.
'169.254.170.2', // AWS task metadata endpoint.
'192.0.0.192', // Oracle Cloud endpoint.
'100.100.100.200', // Alibaba Cloud endpoint.
);
if ( in_array( $ip, $blocked_ips, true ) ) {
return new WP_Error( 'table_import_url_host_blocked', '', array( 'url' => $this->import_config['url'], 'ip' => $ip ) );
}
/**
* Load WP file functions to be sure that `download_url()` exists, in particular during Cron requests.
*/
require_once ABSPATH . 'wp-admin/includes/file.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
// Download URL to local file.
$location = download_url( $this->import_config['url'] );
if ( is_wp_error( $location ) ) {
$error = new WP_Error( 'table_import_url_download_failed', '', $this->import_config['url'] );
$error->merge_from( $location );
return $error;
}
$import_files[] = new File( array(
'location' => $location,
'name' => $this->import_config['url'],
) );
break;
case 'server':
if ( ABSPATH === $this->import_config['server'] ) {
return new WP_Error( 'table_import_server_invalid', '', $this->import_config['server'] );
}
if ( ! is_readable( $this->import_config['server'] ) ) {
return new WP_Error( 'table_import_server_not_readable', '', $this->import_config['server'] );
}
$import_files[] = new File( array(
'location' => $this->import_config['server'],
'name' => pathinfo( $this->import_config['server'], PATHINFO_BASENAME ),
'keep_file' => true, // Files on the server must not be deleted.
) );
break;
case 'form-field':
$location = wp_tempnam();
$num_written_bytes = file_put_contents( $location, $this->import_config['form-field'] );
if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
@unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
return new WP_Error( 'table_import_form-field_temp_file_not_written' );
}
$import_files[] = new File( array(
'location' => $location,
'name' => __( 'Imported from Manual Input', 'tablepress' ),
) );
break;
default:
return new WP_Error( 'table_import_invalid_source', '', $this->import_config['source'] );
}
return $import_files;
}
/**
* Replaces ZIP archives in the import files with a list of their contents.
*
* ZIP files are removed from the list and their contents are added to the end of the list.
*
* @since 2.0.0
*
* @param File[] $import_files Files that shall be imported, including ZIP archives.
* @return File[] Files that shall be imported, with all ZIP archives recursively replaced by their contents.
*/
protected function convert_zip_files( array $import_files ): array {
foreach ( $import_files as $key => &$file ) {
// $file has to be used by reference, so that $key points to the correct element, due to array modification with `unset()` and `array_push()`.
// Skip files that already have an error.
if ( is_wp_error( $file->error ) ) {
continue;
}
$file->extension = strtolower( pathinfo( $file->name, PATHINFO_EXTENSION ) );
if ( function_exists( 'mime_content_type' ) ) {
$mime_type = mime_content_type( $file->location );
if ( false !== $mime_type ) {
$file->mime_type = $mime_type;
}
}
// Detect ZIP files from their file extension or MIME type.
if ( 'zip' === $file->extension || 'application/zip' === $file->mime_type ) {
$extracted_files = $this->extract_zip_file( $file );
if ( is_wp_error( $extracted_files ) ) {
$file->error = $extracted_files;
$this->maybe_unlink_file( $file );
continue;
}
if ( empty( $extracted_files ) ) {
$file->error = new WP_Error( 'table_import_zip_file_empty', '', $file->name );
$this->maybe_unlink_file( $file );
continue;
}
/*
* Remove the ZIP file from the list and instead append its contents.
* Appending ensures recursiveness, as the appended files will be checked again.
*/
unset( $import_files[ $key ] );
array_push( $import_files, ...$extracted_files );
$this->maybe_unlink_file( $file );
}
}
unset( $file ); // Unset use-by-reference parameter of foreach loop.
$import_files = array_merge( $import_files ); // Re-index.
return $import_files;
}
/**
* Extracts the files of a ZIP file and returns a list of files and their location.
*
* Depending on availability, either the PHP's ZipArchive class or WordPress' PclZip class is used.
*
* @since 2.0.0
*
* @param File $zip_file File data of a ZIP file (likely in a temporary folder).
* @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
*/
protected function extract_zip_file( File $zip_file ) /* : array|WP_Error */ {
if ( class_exists( 'ZipArchive', false ) ) {
$ziparchive_result = $this->extract_zip_file_ziparchive( $zip_file );
if ( is_array( $ziparchive_result ) ) {
return $ziparchive_result;
}
} else {
$ziparchive_result = new WP_Error( 'table_import_error_zip_open', '', array( 'ziparchive_error' => 'Class ZipArchive not available' ) );
}
// Fall through to PclZip if ZipArchive is not available or encountered an error opening the file.
$pclzip_result = $this->extract_zip_file_pclzip( $zip_file );
if ( is_wp_error( $pclzip_result ) ) {
// Append the WP_Error from ZipArchive, to have all error information available.
$pclzip_result->merge_from( $ziparchive_result );
}
return $pclzip_result;
}
/**
* Extracts the files of a ZIP file using the PHP ZipArchive class.
*
* The ZIP file is extracted to a temporary folder and a list of files and their location is returned.
*
* @since 2.3.0
*
* @param File $zip_file File data of a ZIP file (likely in a temporary folder).
* @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
*/
protected function extract_zip_file_ziparchive( File $zip_file ) /* : array|WP_Error */ {
$archive = new ZipArchive();
$archive_opened = $archive->open( $zip_file->location, ZipArchive::CHECKCONS );
// If the ZIP file can't be opened with ZipArchive::CHECKCONS, try again without.
if ( true !== $archive_opened ) {
$archive_opened = $archive->open( $zip_file->location );
}
// If the ZIP file can't even be opened without ZipArchive::CHECKCONS, bail.
if ( true !== $archive_opened ) {
return new WP_Error( 'table_import_error_zip_open', '', array( 'ziparchive_error' => $archive_opened ) );
}
$files = array();
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
for ( $file_idx = 0; $file_idx < $archive->numFiles; $file_idx++ ) {
$file_name = $archive->getNameIndex( $file_idx );
if ( false === $file_name ) {
$files[] = new File( array(
'error' => new WP_Error( 'table_import_error_zip_stat', '', array( 'ziparchive_file_index' => $file_idx ) ),
) );
continue;
}
// Skip directories.
if ( str_ends_with( $file_name, '/' ) ) {
continue;
}
// Skip the __MACOSX directory that macOS adds to archives.
if ( str_starts_with( $file_name, '__MACOSX/' ) ) {
continue;
}
// Don't extract invalid files.
if ( 0 !== validate_file( $file_name ) ) {
continue;
}
$file_data = $archive->getFromIndex( $file_idx );
if ( false === $file_data ) {
$files[] = new File( array(
'name' => $file_name,
'error' => new WP_Error( 'table_import_error_zip_get_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
) );
continue;
}
$location = wp_tempnam();
$num_written_bytes = file_put_contents( $location, $file_data );
if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
@unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$files[] = new File( array(
'name' => $file_name,
'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file_idx, 'ziparchive_file_name' => $file_name ) ),
) );
continue;
}
$files[] = new File( array(
'location' => $location,
'name' => $file_name,
) );
}
$archive->close();
return $files;
}
/**
* Extracts the files of a ZIP file using WordPress' PclZip class.
*
* The ZIP file is extracted to a temporary folder and a list of files and their location is returned.
*
* @since 2.3.0
*
* @param File $zip_file File data of a ZIP file (likely in a temporary folder).
* @return File[]|WP_Error List of files to import that were extracted from the ZIP file or WP_Error on failure.
*/
protected function extract_zip_file_pclzip( File $zip_file ) /* : array|WP_Error */ {
mbstring_binary_safe_encoding();
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; // @phpstan-ignore requireOnce.fileNotFound (This is a WordPress core file that always exists.)
$archive = new PclZip( $zip_file->location );
$archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING ); // @phpstan-ignore arguments.count (PclZip::extract() uses `func_get_args()` to handle optional arguments.)
reset_mbstring_encoding();
// If the ZIP file can't be opened, bail.
if ( ! is_array( $archive_files ) ) {
return new WP_Error( 'table_import_error_zip_open', '', array( 'pclzip_error' => $archive->errorInfo( true ) ) );
}
$files = array();
foreach ( $archive_files as $file ) {
// Skip directories.
if ( $file['folder'] ) {
continue;
}
// Skip the __MACOSX directory that macOS adds to archives.
if ( str_starts_with( $file['filename'], '__MACOSX/' ) ) {
continue;
}
// Don't extract invalid files.
if ( 0 !== validate_file( $file['filename'] ) ) {
continue;
}
$location = wp_tempnam();
$num_written_bytes = file_put_contents( $location, $file['content'] );
if ( false === $num_written_bytes || 0 === $num_written_bytes ) {
@unlink( $location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
$files[] = new File( array(
'name' => $file['filename'],
'error' => new WP_Error( 'table_import_error_zip_write_temp_data', '', array( 'ziparchive_file_index' => $file['index'], 'ziparchive_file_name' => $file['filename'] ) ),
) );
continue;
}
$files[] = new File( array(
'location' => $location,
'name' => $file['filename'],
) );
}
return $files;
}
/**
* Deletes a file unless the `keep_file` property is set to `true`.
*
* @since 2.0.0
*
* @param File $file File that should maybe be deleted.
*/
protected function maybe_unlink_file( File $file ): void {
if ( ! $file->keep_file && file_exists( $file->location ) ) {
@unlink( $file->location ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
}
}
/**
* Prepares a list of table names/IDs for use when replacing/appending existing tables (except for the JSON format).
*
* @since 2.0.0
*
* @return array List of table names and IDs.
*/
protected function get_list_of_table_names(): array {
$existing_tables = array();
// Load all table IDs and names for a comparison with the file name.
$table_ids = TablePress::$model_table->load_all( false );
foreach ( $table_ids as $table_id ) {
// Load table, without table data, options, and visibility settings.
$table = TablePress::$model_table->load( $table_id, false, false );
if ( ! is_wp_error( $table ) ) {
$existing_tables[ (string) $table['name'] ][] = $table_id; // Attention: The table name is not unique!
}
}
return $existing_tables;
}
/**
* Checks whether the requirements for the PHPSpreadsheet import class are fulfilled or if the legacy import class should be used.
*
* @since 2.0.0
*
* @return bool Whether the legacy import class should be used.
*/
protected function should_use_legacy_import_class(): bool {
// Allow overriding in the import config (coming e.g. from the import form UI).
if ( $this->import_config['legacy_import'] ) {
return true;
}
/**
* Filters whether the Legacy Table Import class shall be used.
*
* @since 2.0.0
*
* @param bool $use_legacy_class Whether to use the legacy table import class. Default false.
*/
if ( apply_filters( 'tablepress_use_legacy_table_import_class', false ) ) {
return true;
}
// Use the legacy import class, if the requirements for PHPSpreadsheet are not fulfilled.
$phpspreadsheet_requirements_fulfilled = extension_loaded( 'mbstring' )
&& class_exists( 'ZipArchive', false )
&& class_exists( 'DOMDocument', false )
&& function_exists( 'simplexml_load_string' )
&& ( function_exists( 'libxml_disable_entity_loader' ) || PHP_VERSION_ID >= 80000 ); // This function is only needed for older versions of PHP.
if ( ! $phpspreadsheet_requirements_fulfilled ) {
return true;
}
return false;
}
/**
* Imports all found/extracted/configured files into TablePress.
*
* @since 2.0.0
*
* @param File[] $import_files Files that shall be imported.
* @return array{tables: array>, errors: File[]} Imported tables and files that caused errors.
*/
protected function import_files( array $import_files ): array {
$tables = array();
$errors = array();
$use_legacy_import_class = $this->should_use_legacy_import_class();
// Load Import Base Class.
TablePress::load_file( 'class-import-base.php', 'classes' );
// Choose the Table Import library based on the PHP version and the filter hook value.
if ( $use_legacy_import_class ) {
// @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
$this->importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
} else {
// @phpstan-ignore assign.propertyType (The `load_class()` method returns `object` and not a specific type.)
$this->importer = TablePress::load_class( 'TablePress_Import_PHPSpreadsheet', 'class-import-phpspreadsheet.php', 'classes' );
}
// If there is more than one valid import file, ignore the chosen existing table for replacing/appending.
if ( in_array( $this->import_config['type'], array( 'replace', 'append' ), true ) && '' !== $this->import_config['existing_table'] ) {
$valid_import_files = 0;
foreach ( $import_files as $file ) {
if ( ! is_wp_error( $file->error ) ) {
++$valid_import_files;
if ( $valid_import_files > 1 ) {
$this->import_config['existing_table'] = '';
break;
}
}
}
}
// Loop through all import files and import them.
foreach ( $import_files as $file ) {
if ( is_wp_error( $file->error ) ) {
$errors[] = $file;
continue;
}
// Use import method depending on chosen import class.
if ( $use_legacy_import_class ) {
$table = $this->load_table_from_file_legacy( $file );
} else {
$table = $this->load_table_from_file_phpspreadsheet( $file );
}
$this->maybe_unlink_file( $file );
if ( is_wp_error( $table ) ) {
$file->error = $table;
$errors[] = $file;
continue;
}
$table = $this->save_imported_table( $table, $file );
if ( is_wp_error( $table ) ) {
$file->error = $table;
$errors[] = $file;
continue;
}
$tables[] = $table;
}
return array(
'tables' => $tables,
'errors' => $errors,
);
}
/**
* Loads a table from a file via the legacy import class.
*
* @since 2.0.0
*
* @param File $file File with the table data.
* @return array|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
*/
protected function load_table_from_file_legacy( File $file ) /* : array|WP_Error */ {
// Guess the import format from the file extension.
switch ( $file->extension ) {
case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet.
case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded).
case 'xltx': // Excel (OfficeOpenXML) Template.
case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded).
$format = 'xlsx';
break;
case 'xls': // Excel (BIFF) Spreadsheet.
case 'xlt': // Excel (BIFF) Template.
$format = 'xls';
break;
case 'htm':
case 'html':
$format = 'html';
break;
case 'csv':
case 'tsv':
$format = 'csv';
break;
case 'json':
$format = 'json';
break;
default:
// If no format was found, try finding the format from the first character below.
$format = '';
}
$data = file_get_contents( $file->location );
if ( false === $data ) {
return new WP_Error( 'table_import_legacy_data_read', '', $file->location );
}
if ( '' === $data ) {
return new WP_Error( 'table_import_legacy_data_empty', '', $file->location );
}
// If no format could be determined from the file extension, try guessing from the file content.
if ( '' === $format ) {
$data = trim( $data );
$first_character = $data[0];
$last_character = $data[-1];
if ( '<' === $first_character && '>' === $last_character ) {
$format = 'html';
} elseif ( ( '[' === $first_character && ']' === $last_character ) || ( '{' === $first_character && '}' === $last_character ) ) {
$json_table = json_decode( $data, true );
if ( ! is_null( $json_table ) ) {
$format = 'json';
}
}
}
// Fall back to CSV if no file format could be determined.
if ( '' === $format ) {
$format = 'csv';
}
if ( ! in_array( $format, $this->importer->import_formats, true ) ) { // @phpstan-ignore property.notFound (`$this->importer` is an instance of `TablePress_Import_Legacy` which has the property `import_formats`.)
return new WP_Error( 'table_import_legacy_unknown_format', '', $file->name );
}
$table = $this->importer->import_table( $format, $data );
if ( false === $table ) {
return new WP_Error( 'table_import_legacy_importer_failed', '', array( 'file_name' => $file->name, 'file_format' => $format ) );
}
return $table;
}
/**
* Loads a table from a file via the PHPSpreadsheet import class.
*
* @since 2.0.0
*
* @param File $file File with the table data.
* @return array|WP_Error Loaded table on success (either with all properties or just 'data'), WP_Error on failure.
*/
protected function load_table_from_file_phpspreadsheet( File $file ) /* : array|WP_Error */ {
// Convert File object to array, as those are not yet used outside of this class.
return $this->importer->import_table( $file ); // @phpstan-ignore return.type (This is an instance of TablePress_Import_PHPSpreadsheet which does not return false.)
}
/**
* Imports a loaded table into TablePress.
*
* @since 2.0.0
*
* @param array $table The table to be imported, either with properties or just the $table['data'] property set.
* @param File $file File with the table data.
* @return array|WP_Error Imported table on success, WP_Error on failure.
*/
protected function save_imported_table( array $table, File $file ) /* : array|WP_Error */ {
// If name and description are imported from a new table, use those.
if ( ! isset( $table['name'] ) ) {
$table['name'] = $file->name;
}
if ( ! isset( $table['description'] ) ) {
$table['description'] = $file->name;
}
$import_type = $this->import_config['type'];
$existing_table_id = $this->import_config['existing_table'];
// If no existing table ID has been set (or if we are importing multiple tables), try to find a potential existing table from the table ID in the import data or by comparing the file name with the table name.
if ( in_array( $import_type, array( 'replace', 'append' ), true ) && '' === $existing_table_id ) {
if ( isset( $table['id'] ) ) {
// If the table already contained a table ID (e.g. for the JSON format), use that.
$existing_table_id = $table['id'];
} elseif ( isset( $this->table_names_ids[ $file->name ] ) && 1 === count( $this->table_names_ids[ $file->name ] ) ) {
// Use the replace/append ID of tables where the table name matches the file name, but only if there was exactly one file name match.
$existing_table_id = $this->table_names_ids[ $file->name ][0];
}
}
// If the table that is to be replaced or appended to does not exist, add the new table instead.
if ( ! TablePress::$model_table->table_exists( $existing_table_id ) ) {
$existing_table_id = '';
$import_type = 'add';
}
$table = $this->import_tablepress_table( $table, $import_type, $existing_table_id );
return $table;
}
/**
* Imports a table by either replacing or appending to an existing table or by adding it as a new table.
*
* @since 1.0.0
*
* @param array $imported_table The table to be imported, either with properties or just the `name`, `description`, and `data` property set.
* @param string $import_type What to do with the imported data: "add", "replace", "append".
* @param string $existing_table_id Empty string if table shall be added as a new table, ID of the table to be replaced or appended to otherwise.
* @return array|WP_Error Table on success, WP_Error on error.
*/
protected function import_tablepress_table( array $imported_table, string $import_type, string $existing_table_id ) /* : array|WP_Error */ {
// Full JSON format table can contain a table ID, try to keep that, by later changing the imported table ID to this.
$table_id_in_import = $imported_table['id'] ?? '';
// To be able to replace or append to a table, the user must be able to edit the table, or it must be a request via the Automatic Periodic Table Import module.
if ( in_array( $import_type, array( 'replace', 'append' ), true )
&& ! ( current_user_can( 'tablepress_edit_table', $existing_table_id ) || doing_action( 'tablepress_automatic_periodic_table_import_action' ) ) ) {
return new WP_Error( 'table_import_replace_append_capability_check_failed', '', $existing_table_id );
}
switch ( $import_type ) {
case 'add':
$existing_table = TablePress::$model_table->get_table_template();
// Import visibility information if it exists, usually only for the JSON format.
if ( isset( $imported_table['visibility'] ) ) {
$existing_table['visibility'] = $imported_table['visibility'];
}
break;
case 'replace':
// Load table, without table data, but with options and visibility settings.
$existing_table = TablePress::$model_table->load( $existing_table_id, false, true );
if ( is_wp_error( $existing_table ) ) {
$error = new WP_Error( 'table_import_replace_table_load', '', $existing_table_id );
$error->merge_from( $existing_table );
return $error;
}
// Don't change name and description when a table is replaced.
$imported_table['name'] = $existing_table['name'];
$imported_table['description'] = $existing_table['description'];
// Replace visibility information if it exists.
if ( isset( $imported_table['visibility'] ) ) {
$existing_table['visibility'] = $imported_table['visibility'];
}
break;
case 'append':
// Load table, with table data, options, and visibility settings.
$existing_table = TablePress::$model_table->load( $existing_table_id, true, true );
if ( is_wp_error( $existing_table ) ) {
$error = new WP_Error( 'table_import_append_table_load', '', $existing_table_id );
$error->merge_from( $existing_table );
return $error;
}
if ( isset( $existing_table['is_corrupted'] ) && $existing_table['is_corrupted'] ) {
return new WP_Error( 'table_import_append_table_load_corrupted', '', $existing_table_id );
}
// Don't change name and description when a table is appended to.
$imported_table['name'] = $existing_table['name'];
$imported_table['description'] = $existing_table['description'];
// Actual appending:.
$imported_table['data'] = array_merge( $existing_table['data'], $imported_table['data'] );
$this->importer->pad_array_to_max_cols( $imported_table['data'] );
// Append visibility information for rows.
if ( isset( $imported_table['visibility']['rows'] ) ) {
$existing_table['visibility']['rows'] = array_merge( $existing_table['visibility']['rows'], $imported_table['visibility']['rows'] );
}
// When appending, do not overwrite options, e.g. coming from a JSON file.
unset( $imported_table['options'] );
break;
default:
return new WP_Error( 'table_import_import_type_invalid', '', $import_type );
}
// Merge new or existing table with information from the imported table.
$imported_table['id'] = $existing_table['id']; // Will be false for new table or the existing table ID.
// Cut visibility array (if the imported table is smaller), and pad correctly if imported table is bigger than existing table (or new template).
$num_rows = count( $imported_table['data'] );
$num_columns = count( $imported_table['data'][0] );
$imported_table['visibility'] = array(
'rows' => array_pad( array_slice( $existing_table['visibility']['rows'], 0, $num_rows ), $num_rows, 1 ),
'columns' => array_pad( array_slice( $existing_table['visibility']['columns'], 0, $num_columns ), $num_columns, 1 ),
);
// Check if the new table data is valid and consistent.
$table = TablePress::$model_table->prepare_table( $existing_table, $imported_table, false );
if ( is_wp_error( $table ) ) {
$error = new WP_Error( 'table_import_table_prepare', '', $imported_table['id'] );
$error->merge_from( $table );
return $error;
}
// DataTables Custom Commands can only be edit by trusted users.
if ( ! current_user_can( 'unfiltered_html' ) ) {
$table['options']['datatables_custom_commands'] = $existing_table['options']['datatables_custom_commands'];
}
// Replace existing table or add new table.
if ( in_array( $import_type, array( 'replace', 'append' ), true ) ) {
// Replace existing table with imported/appended table.
$table_id = TablePress::$model_table->save( $table );
} else {
// Add the imported table (and get its first ID).
$table_id = TablePress::$model_table->add( $table );
}
if ( is_wp_error( $table_id ) ) {
$error = new WP_Error( 'table_import_table_save_or_add', '', $table['id'] );
$error->merge_from( $table_id );
return $error;
}
// Try to use ID from imported file (e.g. in full JSON format table).
if ( '' !== $table_id_in_import && $table_id !== $table_id_in_import && current_user_can( 'tablepress_edit_table_id', $table_id ) ) {
$id_changed = TablePress::$model_table->change_table_id( $table_id, $table_id_in_import );
if ( ! is_wp_error( $id_changed ) ) {
$table_id = $table_id_in_import;
}
}
$table['id'] = $table_id;
return $table;
}
/**
* Imports a table in legacy versions of the Table Auto Update Extension.
*
* This method is deprecated and is only left for backward compatibility reasons. Do not use this in new code!
*
* @since 1.0.0
* @deprecated 2.0.0 Use `run()` instead.
*
* @param string $format Import format.
* @param string $data Data to import.
* @return array|WP_Error|false Table array on success, WP_Error or false on error.
*/
public function import_table( string $format, string $data ) /* : array|false */ {
TablePress::load_file( 'class-import-base.php', 'classes' );
$importer = TablePress::load_class( 'TablePress_Import_Legacy', 'class-import-legacy.php', 'classes' );
return $importer->import_table( $format, $data );
}
} // class TablePress_Import
class-elementor-widget-table.php 0000644 00000020753 15021224011 0012713 0 ustar 00 > Widget stack.
*/
public function get_stack( $with_common_controls = true ) {
$stack = parent::get_stack( $with_common_controls ); // @phpstan-ignore staticMethod.notFound (Elementor methods are not in the stubs.)
unset( $stack['tabs']['advanced'] );
return $stack;
}
/**
* Gets the widget upsale data.
*
* @since 3.1.0
*
* @return array Widget upsale data.
*/
protected function get_upsale_data(): array {
return array(
'condition' => tb_tp_fs()->is_free_plan(),
'image' => plugins_url( 'admin/img/tablepress.svg', TABLEPRESS__FILE__ ),
'image_alt' => esc_attr__( 'Upgrade to TablePress Pro!', 'tablepress' ),
'title' => esc_html__( 'Upgrade to TablePress Pro!', 'tablepress' ),
'description' => esc_html__( 'Check out the TablePress premium versions and give your tables super powers!', 'tablepress' ),
'upgrade_url' => 'https://tablepress.org/premium/?utm_source=plugin&utm_medium=button&utm_content=elementor-widget',
'upgrade_text' => esc_html__( 'Upgrade Now', 'tablepress' ),
);
}
/**
* Gets whether the widget requires an inner wrapper.
*
* This is used to determine whether to optimize the DOM size.
*
* @since 3.1.0
*
* @return bool Whether to optimize the DOM size.
*/
public function has_widget_inner_wrapper(): bool {
return false;
}
/**
* Gets whether the element returns dynamic content.
*
* This is used to determine whether to cache the element output or not.
*
* @since 3.1.0
*
* @return bool Whether to cache the element output.
*/
protected function is_dynamic_content(): bool {
return true;
}
/**
* Registers the widget controls.
*
* Adds input fields to allow the user to customize the widget settings.
*
* @since 3.1.0
*/
protected function register_controls(): void {
$this->start_controls_section( // @phpstan-ignore method.notFound (Elementor methods are not in the stubs.)
'table',
array(
'label' => esc_html__( 'Table', 'tablepress' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT, // @phpstan-ignore classConstant.notFound (Elementor constants are not in the stubs.)
),
);
$tables = array();
// Load all table IDs without priming the post meta cache, as table options/visibility are not needed.
$table_ids = \TablePress::$model_table->load_all( false );
foreach ( $table_ids as $table_id ) {
// Load table, without table data, options, and visibility settings.
$table = \TablePress::$model_table->load( $table_id, false, false );
// Skip tables that could not be loaded.
if ( is_wp_error( $table ) ) {
continue;
}
if ( '' === trim( $table['name'] ) ) {
$table['name'] = __( '(no name)', 'tablepress' );
}
$tables[ $table_id ] = esc_html( sprintf( __( 'ID %1$s: %2$s', 'tablepress' ), $table_id, $table['name'] ) );
}
/**
* Filters the list of table IDs and names that is passed to the block editor, and is then used in the dropdown of the TablePress table block.
*
* @since 2.0.0
*
* @param array $tables List of table names, the table ID is the array key.
*/
$tables = apply_filters( 'tablepress_block_editor_tables_list', $tables );
$this->add_control( // @phpstan-ignore method.notFound
'table_id',
array(
'label' => esc_html__( 'Table:', 'tablepress' ),
'show_label' => false,
'label_block' => true,
'description' => esc_html__( 'Select the TablePress table that you want to embed.', 'tablepress' )
. ( current_user_can( 'tablepress_list_tables' ) ? sprintf( ' %2$s', esc_url( \TablePress::url( array( 'action' => 'list' ) ) ), esc_html__( 'Manage your tables.', 'tablepress' ) ) : '' ),
'type' => \Elementor\Controls_Manager::SELECT2, // @phpstan-ignore classConstant.notFound (Elementor constants are not in the stubs.)
'ai' => array( 'active' => false ),
'options' => $tables,
),
);
$this->end_controls_section(); // @phpstan-ignore method.notFound (Elementor methods are not in the stubs.)
$this->start_controls_section( // @phpstan-ignore method.notFound (Elementor methods are not in the stubs.)
'advanced',
array(
'label' => esc_html__( 'Advanced', 'tablepress' ),
'tab' => \Elementor\Controls_Manager::TAB_CONTENT, // @phpstan-ignore classConstant.notFound (Elementor constants are not in the stubs.)
'condition' => array(
'id!' => '',
),
),
);
$this->add_control( // @phpstan-ignore method.notFound (Elementor methods are not in the stubs.)
'parameters',
array(
'label' => esc_html__( 'Configuration parameters:', 'tablepress' ),
'label_block' => true,
'description' => esc_html( __( 'These additional parameters can be used to modify specific table features.', 'tablepress' ) . ' ' . __( 'See the TablePress Documentation for more information.', 'tablepress' ) ),
'type' => \Elementor\Controls_Manager::TEXT, // @phpstan-ignore classConstant.notFound (Elementor constants are not in the stubs.)
'input_type' => 'text',
'placeholder' => '',
'ai' => array( 'active' => false ),
),
);
$this->end_controls_section(); // @phpstan-ignore method.notFound (Elementor methods are not in the stubs.)
}
/**
* Render oEmbed widget output on the frontend.
*
* Written in PHP and used to generate the final HTML.
*
* @since 3.1.0
*/
protected function render(): void {
$settings = $this->get_settings_for_display(); // @phpstan-ignore method.notFound (Elementor methods are not in the stubs.)
// Don't return anything if no table was selected.
if ( empty( $settings['table_id'] ) ) {
/*
* In TablePress 3.1 (before 3.1.1), the widget control was named "id" instead of "table_id", which however caused problems.
* To ensure that tables will continue to be shown, if the widget was created with 3.1, the "table_id" is set to the "id" value, if only that exists.
*/
if ( empty( $settings['id'] ) ) {
return;
} else {
$settings['table_id'] = $settings['id'];
}
}
if ( '' !== trim( $settings['parameters'] ) ) {
$render_attributes = shortcode_parse_atts( $settings['parameters'] );
} else {
$render_attributes = array();
}
$render_attributes['id'] = $settings['table_id'];
/*
* It would be nice to print only the Shortcode, for better data portability, e.g. if a site switches away from Elementor.
* However, the editor will then only render the Shortcode itself, which is not very helpful.
* Due to this, the table HTML code is rendered.
* echo '[' . \TablePress::$shortcode . " id={$settings['table_id']} {$settings['parameters']} /]";
*/
echo \TablePress::$controller->shortcode_table( $render_attributes );
}
} // class TablePressTableWidget
class-admin-page-helper.php 0000644 00000015024 15021224011 0011625 0 ustar 00 $script_data Optional. JS data that is printed to the page before the script is included. The array key will be used as the name, the value will be JSON encoded.
*/
public function enqueue_script( string $name, array $dependencies = array(), array $script_data = array() ): void {
$js_file = "admin/js/build/{$name}.js";
$js_url = plugins_url( $js_file, TABLEPRESS__FILE__ );
$version = TablePress::version;
// Load dependencies and version from the auto-generated asset PHP file.
$script_asset_path = TABLEPRESS_ABSPATH . "admin/js/build/{$name}.asset.php";
if ( file_exists( $script_asset_path ) ) {
$script_asset = require $script_asset_path;
if ( isset( $script_asset['dependencies'] ) ) {
$dependencies = array_merge( $dependencies, $script_asset['dependencies'] );
}
if ( isset( $script_asset['version'] ) ) {
$version = $script_asset['version'];
}
}
/*
* Register the `react-jsx-runtime` polyfill, if it is not already registered.
* This is needed as a polyfill for WP < 6.6, and can be removed once WP 6.6 is the minimum requirement for TablePress.
*/
if ( ! wp_script_is( 'react-jsx-runtime', 'registered' ) ) {
wp_register_script( 'react-jsx-runtime', plugins_url( 'admin/js/react-jsx-runtime.min.js', TABLEPRESS__FILE__ ), array( 'react' ), TablePress::version, true );
}
/**
* Filters the dependencies of a TablePress script file.
*
* @since 2.0.0
*
* @param string[] $dependencies List of the dependencies that the $name script relies on.
* @param string $name Name of the JS script, without extension.
*/
$dependencies = apply_filters( 'tablepress_admin_page_script_dependencies', $dependencies, $name );
wp_enqueue_script( "tablepress-{$name}", $js_url, $dependencies, $version, true );
// Load JavaScript translation files, for all scripts that rely on `wp-i18n`.
if ( in_array( 'wp-i18n', $dependencies, true ) ) {
wp_set_script_translations( "tablepress-{$name}", 'tablepress' );
}
if ( ! empty( $script_data ) ) {
foreach ( $script_data as $var_name => $var_data ) {
$var_data = wp_json_encode( $var_data, JSON_FORCE_OBJECT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
wp_add_inline_script( "tablepress-{$name}", "const tablepress_{$var_name} = {$var_data};", 'before' );
}
}
}
/**
* Register a filter hook on the admin footer.
*
* @since 1.0.0
*/
public function add_admin_footer_text(): void {
// Show admin footer message (only on TablePress admin screens).
add_filter( 'admin_footer_text', array( $this, '_admin_footer_text' ) );
}
/**
* Adds a TablePress "Thank You" message to the admin footer content.
*
* @since 1.0.0
*
* @param string $content Current admin footer content.
* @return string New admin footer content.
*/
public function _admin_footer_text( /* string */ $content ): string {
// Don't use a type hint in the method declaration as many WordPress plugins use the `admin_footer_text` filter in the wrong way.
// Protect against other plugins not returning a string in their filter callbacks.
if ( ! is_string( $content ) ) { // @phpstan-ignore function.alreadyNarrowedType (The `is_string()` check is needed as the input is coming from a filter hook.)
$content = '';
}
$content .= ' • ' . sprintf( __( 'Thank you for using TablePress.', 'tablepress' ), 'https://tablepress.org/' );
if ( tb_tp_fs()->is_free_plan() ) {
$content .= ' ' . sprintf( __( 'Take a look at the Premium features!', 'tablepress' ), 'https://tablepress.org/premium/?utm_source=plugin&utm_medium=textlink&utm_content=admin-footer' );
}
return $content;
}
/**
* Print the JavaScript code for a WP feature pointer.
*
* @since 1.0.0
*
* @param string $pointer_id The pointer ID.
* @param string $selector The HTML elements, on which the pointer should be attached.
* @param array $args Arguments to be passed to the pointer JS (see wp-pointer.js).
*/
public function print_wp_pointer_js( string $pointer_id, string $selector, array $args ): void {
if ( empty( $pointer_id ) || empty( $selector ) || empty( $args['content'] ) ) {
return;
}
/*
* Print JS code for the feature pointers, extended with event handling for opened/closed "Screen Options", so that pointers can
* be repositioned. 210 ms is slightly slower than jQuery's "fast" value, to allow all elements to reach their original position.
*/
?>
> $table_data Table data in which formulas shall be evaluated.
* @param string $table_id ID of the passed table.
* @return array> Table data with evaluated formulas.
*/
public function evaluate_table_data( array $table_data, string $table_id ): array {
// Choose the Table Evaluate library based on the PHP version and the filter hook value.
if ( $this->_should_use_legacy_evaluate_class() ) {
$evaluate_class = TablePress::load_class( 'TablePress_Evaluate_Legacy', 'class-evaluate-legacy.php', 'classes' );
} else {
$evaluate_class = TablePress::load_class( 'TablePress_Evaluate_PHPSpreadsheet', 'class-evaluate-phpspreadsheet.php', 'classes' );
}
return $evaluate_class->evaluate_table_data( $table_data, $table_id );
}
} // class TablePress_Evaluate