PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / trunk
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin vtrunk
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / Infrastructure / WP / MCP / Abilities / create-simple-table.php

create-simple-table.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin trunk, at Infrastructure/WP/MCP/Abilities/create-simple-table.php

456 lines 20.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: wpdatatables/create-simple-table
4 *
5 * Creates a new "Simple" (spreadsheet) wpDataTable with the specified title,
6 * column headers, and initial row data. Supports custom HTML in cells, cell
7 * styling (colors, fonts, alignment), merged cells, and table templates
8 * (pricing, pedigree, etc.).
9 *
10 * Available on all tiers (Free, Starter, Standard, Pro, Developer).
11 *
12 * @package wpDataTables_MCP_Server
13 * @since 0.2.0
14 */
15
16 defined( 'ABSPATH' ) or die('Access denied.');
17
18 add_action( 'wp_abilities_api_init', 'wdtmcp_register_create_simple_table_ability' );
19
20 function wdtmcp_register_create_simple_table_ability() {
21 wp_register_ability(
22 'wpdatatables/create-simple-table',
23 array(
24 'label' => __( 'Create Simple Table', 'wpdatatables' ),
25 'description' => __( 'Creates a Simple wpDataTable (Handsontable editor, pure HTML, no sorting/filtering). Use when: static or manually curated content, heavy styling (pricing cards, comparison tables, pedigree). Do not use for external URLs or SQL-backed data. For images use {"attachment_id":123,"size":"medium","alt":"..."} in cells; get IDs via list-media or upload-media-from-url. For styled tables use {"data":"<div class=\'panel\'>...</div>","meta":[]} and custom_css. Supports merged_cells, table_settings. WHEN TO CALL: user wants a designed/layout-heavy HTML table, pricing comparison, pedigree chart, or any non-data-source static content. DO NOT USE for: CSV/JSON/API data sources, database queries, or large sortable datasets (creating data-source and SQL tables is a premium wpDataTables feature, not available in Lite via MCP). REQUIRED INPUT: title (string), columns (string[] headers). OPTIONAL: rows (array of row arrays — omit or pass [] to create an empty table skeleton for later editing in wp-admin), table_settings, merged_cells, custom_css. FOR IMAGES: first call list-media or upload-media-from-url, then put attachment_id in cell objects. RETURNS: table_id and shortcode. NEXT STEPS: update-simple-table-styles to refine layout; get-table-info to verify.', 'wpdatatables' ),
26 'category' => 'wpdatatables-data',
27
28 'input_schema' => array(
29 'type' => 'object',
30 'properties' => array(
31 'title' => array(
32 'type' => 'string',
33 'description' => 'Table title.',
34 ),
35 'columns' => array(
36 'type' => 'array',
37 'description' => 'Column header labels.',
38 'items' => array( 'type' => 'string' ),
39 ),
40 'rows' => array(
41 'type' => 'array',
42 'description' => 'Optional array of rows. Omit or pass [] for an empty table (column headers only). Each cell: plain string, {"data":"HTML","meta":[]}, or {"attachment_id":123,"size":"medium","alt":"..."} for images. Use list-media or upload-media-from-url to get attachment_id.',
43 ),
44 'table_settings' => array(
45 'type' => 'object',
46 'description' => 'Optional table layout and display settings.',
47 'properties' => array(
48 'template_id' => array( 'type' => 'integer', 'description' => '0=default, 3=pricing, 6=pedigree.' ),
49 'simple_header' => array( 'type' => 'boolean', 'description' => 'First row as header (th).' ),
50 'stripe_table' => array( 'type' => 'boolean', 'description' => 'Alternating row colors.' ),
51 'responsive' => array( 'type' => 'boolean', 'description' => 'Responsive layout.' ),
52 'remove_borders' => array( 'type' => 'boolean', 'description' => 'Hide cell borders.' ),
53 'cell_padding' => array( 'type' => 'integer', 'description' => 'Cell padding in px.' ),
54 'show_title' => array( 'type' => 'boolean', 'description' => 'Show table title above table.' ),
55 'show_table_description' => array( 'type' => 'boolean', 'description' => 'Show description.' ),
56 'table_description' => array( 'type' => 'string', 'description' => 'Table description text.' ),
57 ),
58 ),
59 'merged_cells' => array(
60 'type' => 'array',
61 'description' => 'Array of merged cell definitions. Each: {row, col, rowspan, colspan}. Row/col are 0-based.',
62 ),
63 'custom_css' => array(
64 'type' => 'string',
65 'description' => 'Custom CSS for heavily styled tables. Use #wpdtSimpleTable-{TABLE_ID} as selector (replace {TABLE_ID} with the returned table_id). Add to page via Custom HTML block or Appearance > Customize > Additional CSS. See wpdatatables.com pricing table tutorials.',
66 ),
67 ),
68 'required' => array( 'title', 'columns' ),
69 ),
70
71 'output_schema' => array(
72 'type' => 'object',
73 'properties' => array(
74 'table_id' => array( 'type' => 'integer', 'description' => 'Created table ID.' ),
75 'shortcode' => array( 'type' => 'string', 'description' => 'WordPress shortcode to embed the table.' ),
76 'custom_css' => array( 'type' => 'string', 'description' => 'Custom CSS to add to the page (if provided). Use #wpdtSimpleTable-{id} with the actual table_id.' ),
77 ),
78 ),
79
80 'execute_callback' => 'wdtmcp_execute_create_simple_table',
81
82 'permission_callback' => function () {
83 return current_user_can( 'manage_options' );
84 },
85
86 'meta' => array(
87 'annotations' => array(
88 'instructions' => __( 'For static HTML tables only. Obtain image attachment_id via list-media or upload-media-from-url before referencing images in cells. Do not use for file, API, or SQL data sources.', 'wpdatatables' ),
89 'readonly' => false,
90 'destructive' => false,
91 'idempotent' => false,
92 ),
93 ),
94 )
95 );
96 }
97
98 /**
99 * Execute callback for wpdatatables/create-simple-table.
100 *
101 * @param array $input
102 * @return array|WP_Error
103 */
104 function wdtmcp_execute_create_simple_table( $input ) {
105 global $wpdb;
106
107 if ( ! class_exists( 'WPDataTableRows' ) || ! class_exists( 'WDTConfigController' ) ) {
108 return new \WP_Error(
109 'wdtmcp_missing_class',
110 __( 'wpDataTables core classes WPDataTableRows and WDTConfigController are required.', 'wpdatatables' )
111 );
112 }
113
114 $title = isset( $input['title'] ) ? sanitize_text_field( $input['title'] ) : '';
115 $columns = isset( $input['columns'] ) && is_array( $input['columns'] ) ? $input['columns'] : array();
116 if ( isset( $input['rows'] ) && ! is_array( $input['rows'] ) ) {
117 return new \WP_Error(
118 'wdtmcp_invalid_rows',
119 __( 'rows must be an array.', 'wpdatatables' )
120 );
121 }
122 $rows = isset( $input['rows'] ) ? $input['rows'] : array();
123 $table_settings = isset( $input['table_settings'] ) && is_array( $input['table_settings'] ) ? $input['table_settings'] : array();
124 $merged_cells = isset( $input['merged_cells'] ) && is_array( $input['merged_cells'] ) ? $input['merged_cells'] : array();
125
126 if ( '' === $title ) {
127 return new \WP_Error( 'wdtmcp_missing_param', __( 'title is required.', 'wpdatatables' ) );
128 }
129
130 $col_count = max( 1, count( $columns ) );
131 $row_count = count( $rows );
132
133 // Normalize column headers.
134 $col_headers = array();
135 for ( $i = 0; $i < $col_count; $i++ ) {
136 $col_headers[] = isset( $columns[ $i ] ) ? sanitize_text_field( (string) $columns[ $i ] ) : ( 'Col' . ( $i + 1 ) );
137 }
138
139 $col_widths = array_fill( 0, $col_count, 100 );
140
141 // Normalize merged cells to wpDataTables format.
142 $merged_cells_normalized = array();
143 foreach ( $merged_cells as $m ) {
144 if ( ! isset( $m['row'], $m['col'], $m['rowspan'], $m['colspan'] ) ) {
145 continue;
146 }
147
148 $row = (int) $m['row'];
149 $col = (int) $m['col'];
150 $rowspan = max( 1, (int) $m['rowspan'] );
151 $colspan = max( 1, (int) $m['colspan'] );
152
153 if ( $row < 0 || $col < 0 || $row >= $row_count || $col >= $col_count ) {
154 continue;
155 }
156
157 if ( $row + $rowspan > $row_count ) {
158 $rowspan = $row_count - $row;
159 }
160 if ( $col + $colspan > $col_count ) {
161 $colspan = $col_count - $col;
162 }
163
164 if ( $rowspan < 1 || $colspan < 1 ) {
165 continue;
166 }
167
168 $merged_cells_normalized[] = (object) array(
169 'row' => $row,
170 'col' => $col,
171 'rowspan' => $rowspan,
172 'colspan' => $colspan,
173 'removed' => false,
174 );
175 }
176
177 // Build tableData structure.
178 $content = new \stdClass();
179 $content->rowNumber = $row_count;
180 $content->colNumber = $col_count;
181 $content->colHeaders = $col_headers;
182 $content->colWidths = $col_widths;
183 $content->mergedCells = $merged_cells_normalized;
184 $content->reloadCounter = 0;
185
186 $table_data = new \stdClass();
187 $table_data->title = $title;
188 $table_data->table_description = isset( $table_settings['table_description'] ) ? sanitize_textarea_field( $table_settings['table_description'] ) : '';
189 $table_data->table_type = 'simple';
190 $table_data->content = $content;
191
192 $table_data = \WDTConfigController::sanitizeTableSettingsSimpleTable( $table_data );
193
194 $wp_data_table_rows = new \WPDataTableRows( $table_data );
195
196 // Table-level settings for advanced_settings.
197 $template_id = isset( $table_settings['template_id'] ) ? (int) $table_settings['template_id'] : 0;
198 $simple_header = isset( $table_settings['simple_header'] ) ? (bool) $table_settings['simple_header'] : false;
199 $stripe_table = isset( $table_settings['stripe_table'] ) ? (bool) $table_settings['stripe_table'] : false;
200 $responsive = isset( $table_settings['responsive'] ) ? (bool) $table_settings['responsive'] : false;
201 $remove_borders = isset( $table_settings['remove_borders'] ) ? (bool) $table_settings['remove_borders'] : false;
202 $cell_padding = isset( $table_settings['cell_padding'] ) ? (int) $table_settings['cell_padding'] : 10;
203 $show_title = isset( $table_settings['show_title'] ) ? (bool) $table_settings['show_title'] : true;
204 $show_desc = isset( $table_settings['show_table_description'] ) ? (bool) $table_settings['show_table_description'] : false;
205
206 $table_content = new \stdClass();
207 $table_content->rowNumber = $wp_data_table_rows->getRowNumber();
208 $table_content->colNumber = $wp_data_table_rows->getColNumber();
209 $table_content->colWidths = $wp_data_table_rows->getColWidths();
210 $table_content->colHeaders = $wp_data_table_rows->getColHeaders();
211 $table_content->reloadCounter = $wp_data_table_rows->getReloadCounter();
212 $table_content->mergedCells = $wp_data_table_rows->getMergeCells();
213
214 $advanced = array(
215 'simpleResponsive' => $responsive ? 1 : 0,
216 'simpleHeader' => $simple_header ? 1 : 0,
217 'stripeTable' => $stripe_table ? 1 : 0,
218 'cellPadding' => $cell_padding,
219 'removeBorders' => $remove_borders ? 1 : 0,
220 'borderCollapse' => 'collapse',
221 'borderSpacing' => 0,
222 'verticalScroll' => 0,
223 'verticalScrollHeight' => 600,
224 'show_table_description' => $show_desc,
225 'show_title' => $show_title ? 1 : 0,
226 'table_description' => sanitize_textarea_field( $wp_data_table_rows->getTableDescription() ),
227 'fixed_header' => 0,
228 'fixed_header_offset' => 0,
229 'fixed_columns' => 0,
230 'fixed_left_columns_number' => 0,
231 'fixed_right_columns_number' => 0,
232 'simple_template_id' => $template_id,
233 'customRowDisplay' => '',
234 'index_column' => 0,
235 );
236
237 foreach ( $rows as $row_index => $row ) {
238 if ( ! is_array( $row ) ) {
239 return new \WP_Error(
240 'wdtmcp_invalid_row',
241 sprintf(
242 /* translators: %d: row index */
243 __( 'rows[%d] must be an array of cells.', 'wpdatatables' ),
244 (int) $row_index
245 )
246 );
247 }
248
249 foreach ( $row as $cell_index => $cell ) {
250 if ( ! wdtmcp_is_valid_simple_cell_input( $cell ) ) {
251 return new \WP_Error(
252 'wdtmcp_invalid_cell',
253 sprintf(
254 /* translators: 1: row index, 2: cell index */
255 __( 'rows[%1$d][%2$d] must be a string, number, boolean, null, or cell object.', 'wpdatatables' ),
256 (int) $row_index,
257 (int) $cell_index
258 )
259 );
260 }
261 }
262 }
263
264 $inserted = $wpdb->insert(
265 $wpdb->prefix . 'wpdatatables',
266 array(
267 'title' => sanitize_text_field( $wp_data_table_rows->getTableName() ),
268 'table_type' => $wp_data_table_rows->getTableType(),
269 'connection' => '',
270 'content' => wp_json_encode( $table_content ),
271 'server_side' => 0,
272 'mysql_table_name' => '',
273 'tabletools_config' => serialize( array(
274 'print' => 1,
275 'copy' => 1,
276 'excel' => 1,
277 'csv' => 1,
278 'pdf' => 0,
279 ) ),
280 'advanced_settings' => wp_json_encode( $advanced ),
281 )
282 );
283
284 if ( false === $inserted ) {
285 return new \WP_Error(
286 'wdtmcp_db_error',
287 __( 'Failed to create table in database.', 'wpdatatables' )
288 );
289 }
290
291 $table_id = (int) $wpdb->insert_id;
292
293 $custom_css = '';
294 if ( array_key_exists( 'custom_css', $input ) ) {
295 $validated = wdtmcp_validate_custom_css_input( $input['custom_css'] );
296 if ( is_wp_error( $validated ) ) {
297 return $validated;
298 }
299 $custom_css = $validated;
300 }
301 $css_substituted = '';
302 if ( '' !== $custom_css ) {
303 $css_substituted = wdtmcp_prepare_table_custom_css_for_storage( $custom_css, $table_id );
304 wdtmcp_set_table_custom_css( $table_id, $css_substituted );
305 }
306
307 // Save row data with optional styling and HTML.
308 // Set explicit row height so Handsontable editor doesn't auto-size to huge content
309 // (rich HTML in cells would otherwise make the first row fill the viewport).
310 $default_row_height = 100;
311 foreach ( $rows as $row_index => $row ) {
312 $row_data = new \stdClass();
313 $row_data->cells = array();
314 $row_data->height = $default_row_height;
315
316 for ( $j = 0; $j < $col_count; $j++ ) {
317 $cell = wdtmcp_build_simple_cell( isset( $row[ $j ] ) ? $row[ $j ] : '', $j );
318 $row_data->cells[ $j ] = $cell;
319 }
320
321 \WDTConfigController::saveRowData( $row_data, $table_id );
322 }
323
324 if ( 0 === $row_count ) {
325 $wp_data_table_rows->saveTableWithEmptyData( $table_id );
326 }
327
328 $out = array(
329 'table_id' => $table_id,
330 'shortcode' => '[wpdatatable id=' . $table_id . ']',
331 );
332 if ( '' !== $css_substituted ) {
333 $out['custom_css'] = $css_substituted;
334 }
335 return $out;
336 }
337
338 /**
339 * Whether a cell value is acceptable for {@see wdtmcp_build_simple_cell()}.
340 *
341 * @param mixed $cell_input
342 * @return bool
343 */
344 function wdtmcp_is_valid_simple_cell_input( $cell_input ) {
345 return null === $cell_input || is_scalar( $cell_input ) || is_array( $cell_input ) || is_object( $cell_input );
346 }
347
348 /**
349 * Build a simple table cell from input (string or object with data/type/meta or attachment_id).
350 *
351 * @param mixed $cell_input String, or object with data/type/meta, or attachment_id/size/alt.
352 * @param int $col_index Column index (unused, for future use).
353 * @return stdClass
354 */
355 function wdtmcp_build_simple_cell( $cell_input, $col_index = 0 ) {
356 $cell = new \stdClass();
357 $cell->hidden = false;
358 $cell->type = 'text';
359
360 $is_array = is_array( $cell_input );
361 $is_obj = is_object( $cell_input );
362 $aid = $is_array ? ( $cell_input['attachment_id'] ?? null ) : ( $is_obj ? ( $cell_input->attachment_id ?? null ) : null );
363
364 if ( null !== $aid && (int) $aid > 0 ) {
365 $img_html = wdtmcp_attachment_to_img( (int) $aid, $cell_input );
366 $cell->data = $img_html ? wdtmcp_sanitize_cell_html( $img_html ) : '';
367 $meta = $is_array ? ( $cell_input['meta'] ?? array() ) : ( $cell_input->meta ?? array() );
368 $cell->meta = is_array( $meta ) ? wdtmcp_sanitize_cell_meta( $meta ) : array();
369 return $cell;
370 }
371
372 $is_cell_obj = $is_obj || ( $is_array && isset( $cell_input['data'] ) );
373 if ( $is_cell_obj ) {
374 $data = $is_obj ? ( $cell_input->data ?? '' ) : ( $cell_input['data'] ?? '' );
375 $cell->data = wdtmcp_sanitize_cell_html( (string) $data );
376 $type = $is_obj ? ( $cell_input->type ?? 'text' ) : ( $cell_input['type'] ?? 'text' );
377 if ( in_array( $type, array( 'text', 'link' ), true ) ) {
378 $cell->type = $type;
379 }
380 $meta = $is_obj ? ( $cell_input->meta ?? array() ) : ( $cell_input['meta'] ?? array() );
381 $cell->meta = is_array( $meta ) ? wdtmcp_sanitize_cell_meta( $meta ) : array();
382 } else {
383 $cell->data = wdtmcp_sanitize_cell_html( (string) $cell_input );
384 $cell->meta = array();
385 }
386
387 return $cell;
388 }
389
390 /**
391 * Resolve attachment ID to img HTML for table cells.
392 *
393 * @param int $attachment_id WordPress attachment ID.
394 * @param array|obj $opts Optional size (thumbnail|medium|large|full), alt.
395 * @return string Empty if attachment invalid.
396 */
397 function wdtmcp_attachment_to_img( $attachment_id, $opts = array() ) {
398 $post = get_post( $attachment_id );
399 if ( ! $post || 'attachment' !== $post->post_type || ! wp_attachment_is_image( $attachment_id ) ) {
400 return '';
401 }
402 $url = wp_get_attachment_image_url( $attachment_id, 'medium' );
403 if ( ! $url ) {
404 return '';
405 }
406 $is_arr = is_array( $opts );
407 $size = $is_arr ? ( $opts['size'] ?? 'medium' ) : ( $opts->size ?? 'medium' );
408 $alt = $is_arr ? ( $opts['alt'] ?? '' ) : ( $opts->alt ?? '' );
409 if ( '' === $alt ) {
410 $alt = (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
411 }
412 $sizes = array( 'thumbnail', 'medium', 'medium_large', 'large', 'full' );
413 if ( in_array( $size, $sizes, true ) ) {
414 $url = wp_get_attachment_image_url( $attachment_id, $size ) ?: $url;
415 }
416 $alt_attr = ' alt="' . esc_attr( (string) $alt ) . '"';
417 return '<img src="' . esc_url( $url ) . '"' . $alt_attr . '>';
418 }
419
420 /**
421 * Sanitize cell content (allows safe HTML for pricing tables, links, etc.).
422 *
423 * @param string $html
424 * @return string
425 */
426 function wdtmcp_sanitize_cell_html( $html ) {
427 if ( current_user_can( 'unfiltered_html' ) ) {
428 return wp_unslash( $html );
429 }
430 return wp_kses_post( $html );
431 }
432
433 /**
434 * Sanitize cell meta class names (allow only known wpdt-* prefixes).
435 *
436 * @param array $meta
437 * @return array
438 */
439 function wdtmcp_sanitize_cell_meta( array $meta ) {
440 $allowed_prefixes = array( 'wpdt-tc-', 'wpdt-bc-', 'wpdt-ff-', 'wpdt-fs-', 'wpdt-sc-', 'wpdt-align-', 'wpdt-valign-', 'wpdt-bold', 'wpdt-italic', 'wpdt-underline', 'wpdt-empty-cell', 'wpdt-merged-cell', 'wpdt-wrap-text', 'wpdt-overflow-text', 'wpdt-clip-text' );
441 $out = array();
442 foreach ( $meta as $m ) {
443 $m = sanitize_html_class( (string) $m );
444 if ( '' === $m ) {
445 continue;
446 }
447 foreach ( $allowed_prefixes as $prefix ) {
448 if ( strpos( $m, $prefix ) === 0 || $m === $prefix ) {
449 $out[] = $m;
450 break;
451 }
452 }
453 }
454 return $out;
455 }
456