ultimate_helper.php000064400000021767150212226260010453 0ustar00 $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.php000064400000114672150212240110007636 0ustar00 */ 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'] ) . "\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'] ) . "\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\n"; } } if ( ! empty( $colgroup ) ) { $colgroup = "\n{$colgroup}\n"; } /* * , , and tags. */ if ( ! empty( $thead ) ) { $thead = "\n" . implode( '', $thead ) . "\n"; } else { $thead = ''; } if ( ! empty( $tfoot ) ) { $tfoot = "\n" . implode( '', $tfoot ) . "\n"; } else { $tfoot = ''; } $tbody_classes = array(); if ( $this->render_options['alternating_row_colors'] ) { $tbody_classes[] = 'row-striping'; } if ( $this->render_options['row_hover'] ) { $tbody_classes[] = 'row-hover'; } $tbody_class = implode( ' ', $tbody_classes ); if ( '' !== $tbody_class ) { $tbody_class = ' class="' . esc_attr( $tbody_class ) . '"'; } $tbody = "\n" . implode( '', $tbody ) . "\n"; // Attributes for the table (HTML table element). $table_attributes = array(); // "id" attribute. if ( ! empty( $this->render_options['html_id'] ) ) { $table_attributes['id'] = $this->render_options['html_id']; } // "class" attribute. $css_classes = array( 'tablepress', "tablepress-id-{$this->table['id']}", $this->render_options['extra_css_classes'], ); if ( $this->tbody_has_connected_cells ) { $css_classes[] = 'tbody-has-connected-cells'; } /** * Filters the CSS classes that are given to the HTML table element. * * @since 1.0.0 * * @param string[] $css_classes The CSS classes for the table element. * @param string $table_id The current table ID. */ $css_classes = apply_filters( 'tablepress_table_css_classes', $css_classes, $this->table['id'] ); // $css_classes might contain several classes in one array entry. $css_classes = explode( ' ', implode( ' ', $css_classes ) ); $css_classes = array_map( array( 'TablePress', 'sanitize_css_class' ), $css_classes ); $css_classes = array_unique( $css_classes ); $css_classes = array_filter( $css_classes ); // Remove empty entries. $css_classes = implode( ' ', $css_classes ); if ( '' !== $css_classes ) { $table_attributes['class'] = $css_classes; } // ARIA label attributes. if ( $this->render_options['print_name'] && ! empty( $this->render_options['html_id'] ) ) { $table_attributes['aria-labelledby'] = "{$this->render_options['html_id']}-name"; } if ( $this->render_options['print_description'] && ! empty( $this->render_options['html_id'] ) ) { $table_attributes['aria-describedby'] = "{$this->render_options['html_id']}-description"; } // "summary" attribute. $summary = ''; /** * Filters the content for the summary attribute of the HTML table element. * * The attribute is only added if it is not empty. * * @since 1.0.0 * * @param string $summary The content for the summary attribute of the table. Default empty. * @param array $table The current table. */ $summary = apply_filters( 'tablepress_print_summary_attr', $summary, $this->table ); if ( ! empty( $summary ) ) { $table_attributes['summary'] = esc_attr( $summary ); } // Legacy support for attributes that are not encouraged in HTML5. foreach ( array( 'cellspacing', 'cellpadding', 'border' ) as $attribute ) { if ( false !== $this->render_options[ $attribute ] ) { $table_attributes[ $attribute ] = (int) $this->render_options[ $attribute ]; } } /** * Filters the attributes for the table (HTML table element). * * @since 1.4.0 * * @param array $table_attributes The attributes for the table element. * @param array $table The current table. * @param array $render_options The render options for the table. */ $table_attributes = apply_filters( 'tablepress_table_tag_attributes', $table_attributes, $this->table, $this->render_options ); $table_attributes = $this->_attributes_array_to_string( $table_attributes ); $table_html = "\n"; $table_html .= $caption . $colgroup . $thead . $tbody . $tfoot; $table_html .= ''; /** * 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}"; $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.php000064400000014046150212240110013174 0ustar00> $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.php000064400000037746150212240110007155 0ustar00" 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.php000064400000003303150212240110010565 0ustar00> $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.php000064400000010602150212240110010361 0ustar00option_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.php000064400000021220150212240110011411 0ustar00> */ 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.php000064400000040436150212240110012702 0ustar00|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.php000064400000013703150212240110010533 0ustar00plugin_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.php000064400000001117150212240110007444 0ustar00 */ 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"; array_walk( $row, array( $this, 'html_wrap_and_escape' ), $tag ); $output .= implode( '', $row ); $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}\n"; } } // class TablePress_Export class-import-file.php000064400000002736150212240110010603 0ustar00 $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.php000064400000005765150212240110011435 0ustar00ID, $this->option_name, false ); } } } // class TablePress_WP_User_Option class-import-legacy.php000064400000022707150212240110011130 0ustar00|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.php000064400000000034150212240110006345 0ustar00 */ 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.php000064400000051142150212240110007321 0ustar00 */ 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' ) . '

' . '

TablePress Website

' . '

TablePress FAQ

' . '

TablePress Documentation

' . '

TablePress Support

' ); } /** * 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, '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"; } call_user_func( $box['callback'], $this->data, $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 { ?>
print_nav_tab_menu(); ?>

header_messages as $message ) { echo $message; } // "Import" screen has file upload. $enctype = ( 'import' === $this->action ) ? ' enctype="multipart/form-data"' : ''; ?>
id="tablepress-page-form"> do_text_boxes( 'header' ); $hide_if_no_js = ( in_array( $this->action, array( 'export', 'import' ), true ) ) ? ' class="hide-if-no-js"' : ''; ?>
>
do_text_boxes( 'normal' ); $this->do_meta_boxes( 'normal' ); $this->do_text_boxes( 'additional' ); $this->do_meta_boxes( 'additional' ); // Print all submit buttons. $this->do_text_boxes( 'submit' ); ?>
do_text_boxes( 'side' ); $this->do_meta_boxes( 'side' ); ?>

<?php esc_attr_e( 'TablePress plugin logo', 'tablepress' ); ?>

is_free_plan() ) : ?>
$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.php000064400000076103150212240110007665 0ustar00 */ 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.php000064400000020753150212240110012713 0ustar00> 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.php000064400000015024150212240110011625 0ustar00 $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