├── compatibility └── cmb2.php ├── example.php ├── includes ├── class-ct-database-schema-updater.php ├── class-ct-database-schema.php ├── class-ct-database.php ├── class-ct-edit-view.php ├── class-ct-list-table.php ├── class-ct-list-view.php ├── class-ct-query.php ├── class-ct-rest-controller.php ├── class-ct-rest-meta-fields.php ├── class-ct-table-meta.php ├── class-ct-table.php ├── class-ct-view.php ├── class-ct.php ├── functions.php └── hooks.php ├── init.php └── readme.md /compatibility/cmb2.php: -------------------------------------------------------------------------------- 1 | views->edit->get_slug() ) { 39 | // Let know to this compatibility module it needs to operate 40 | $ct_cmb2_override = true; 41 | } 42 | } 43 | 44 | } 45 | add_action( 'admin_init', 'ct_cmb2_admin_init', 1 ); 46 | 47 | /** 48 | * Check if should override on add meta boxes action 49 | * 50 | * @since 1.0.0 51 | * 52 | * @param $ct_table_name 53 | * @param $object 54 | */ 55 | function ct_cmb2_add_meta_boxes( $ct_table_name, $object ) { 56 | 57 | global $ct_registered_tables, $ct_table, $ct_cmb2_override; 58 | 59 | // If not is a registered table, return 60 | if( ! isset( $ct_registered_tables[$ct_table_name] ) ) { 61 | return; 62 | } 63 | 64 | // If not object given, return 65 | if( ! $object ) { 66 | return; 67 | } 68 | 69 | $primary_key = $ct_table->db->primary_key; 70 | 71 | // Setup a false post var to allow CMB2 trigger cmb2_override_meta_value hook 72 | $_REQUEST['post'] = $object->$primary_key; 73 | 74 | // Let know to this compatibility module it needs to operate 75 | $ct_cmb2_override = true; 76 | 77 | // Fix: CMB2 stop enqueuing their assets so need to add it again 78 | CMB2_Hookup::enqueue_cmb_css(); 79 | CMB2_Hookup::enqueue_cmb_js(); 80 | 81 | } 82 | add_action( 'add_meta_boxes', 'ct_cmb2_add_meta_boxes', 10, 2 ); 83 | 84 | /** 85 | * On save an object, let it know to CMB2 86 | * 87 | * @since 1.0.0 88 | * 89 | * @param $object_id 90 | * @param $object 91 | */ 92 | function ct_cmb2_save_object( $object_id, $object ) { 93 | 94 | global $ct_registered_tables, $ct_table, $ct_cmb2_override; 95 | 96 | // Return if CMB2 not exists 97 | if( ! class_exists( 'CMB2' ) ) { 98 | return; 99 | } 100 | 101 | // Return if user is not allowed 102 | if ( ! current_user_can( $ct_table->cap->edit_item, $object_id ) ) { 103 | return; 104 | } 105 | 106 | // Setup a custom global to meet that we need to override it 107 | $ct_cmb2_override = true; 108 | 109 | // Loop all registered boxes 110 | foreach( CMB2_Boxes::get_all() as $cmb ) { 111 | 112 | // Skip meta boxes that do not support this CT_Table 113 | if( ! in_array( $ct_table->name, $cmb->meta_box['object_types'] ) ) { 114 | continue; 115 | } 116 | 117 | // Take a trip to reading railroad – if you pass go collect $200 118 | $cmb->save_fields( $object_id, 'post', $_POST ); 119 | } 120 | 121 | } 122 | add_action( 'ct_save_object', 'ct_cmb2_save_object', 10, 2 ); 123 | 124 | /** 125 | * Override the CMB2 field value 126 | * 127 | * @since 1.0.0 128 | * 129 | * @param $value 130 | * @param $object_id 131 | * @param $args 132 | * @param $field 133 | * 134 | * @return mixed|string 135 | */ 136 | function ct_cmb2_override_meta_value( $value, $object_id, $args, $field ) { 137 | 138 | global $ct_registered_tables, $ct_table, $ct_cmb2_override; 139 | 140 | if( ! is_a( $ct_table, 'CT_Table' ) ) { 141 | return $value; 142 | } 143 | 144 | if( $ct_cmb2_override !== true ) { 145 | return $value; 146 | } 147 | 148 | $object = (array) ct_get_object( $object_id ); 149 | 150 | // Check if is a main field 151 | if( isset( $object[$args['field_id']] ) ) { 152 | return $object[$args['field_id']]; 153 | } 154 | 155 | // If not is a main field and CT_Table supports meta data, then try to get its value from meta table 156 | if( in_array( 'meta', $ct_table->supports ) ) { 157 | return ct_get_object_meta( $object_id, $args['field_id'], ( $args['single'] || $args['repeat'] ) ); 158 | } 159 | 160 | return ''; 161 | } 162 | add_filter( 'cmb2_override_meta_value', 'ct_cmb2_override_meta_value', 10, 4 ); 163 | 164 | /** 165 | * Override the CMB2 field value save 166 | * 167 | * @since 1.0.0 168 | * 169 | * @param $check 170 | * @param $args 171 | * @param $field_args 172 | * @param $field 173 | * 174 | * @return bool|false|int 175 | */ 176 | function ct_cmb2_override_meta_save( $check, $args, $field_args, $field ) { 177 | 178 | global $ct_registered_tables, $ct_table, $ct_cmb2_override; 179 | 180 | if( $ct_cmb2_override !== true ) { 181 | return $check; 182 | } 183 | 184 | $object = (array) ct_get_object( $args['id'] ); 185 | 186 | // If not is a main field and CT_Table supports meta data, then try to save the given value to the meta table 187 | // Note: Main fields are automatically stored by the save method on the CT_Edit_View edit screen 188 | if( ! isset( $object[$args['field_id']] ) && in_array( 'meta', $ct_table->supports ) ) { 189 | 190 | // Add metadata if not single 191 | if ( ! $args['single'] ) { 192 | return ct_add_object_meta( $args['id'], $args['field_id'], $args['value'], false ); 193 | } 194 | 195 | // Delete meta if we have an empty array 196 | if ( is_array( $args['value'] ) && empty( $args['value'] ) ) { 197 | return ct_delete_object_meta( $args['id'], $args['field_id'], $field->value ); 198 | } 199 | 200 | // Update metadata 201 | return ct_update_object_meta( $args['id'], $args['field_id'], $args['value'] ); 202 | 203 | } 204 | 205 | return $check; 206 | 207 | } 208 | add_filter( 'cmb2_override_meta_save', 'ct_cmb2_override_meta_save', 10, 4 ); 209 | 210 | /** 211 | * Override the CMB2 field value remove 212 | * 213 | * @since 1.0.0 214 | * 215 | * @param $check 216 | * @param $args 217 | * @param $field_args 218 | * @param $field 219 | * 220 | * @return bool|false|int 221 | */ 222 | function ct_cmb2_override_meta_remove( $check, $args, $field_args, $field ) { 223 | 224 | global $ct_registered_tables, $ct_table, $ct_cmb2_override, $wpdb; 225 | 226 | if( $ct_cmb2_override !== true ) { 227 | return $check; 228 | } 229 | 230 | $object = (array) ct_get_object( $args['id'] ); 231 | 232 | // If not is a main field and CT_Table supports meta data, then try to remove the given value to the meta table 233 | // Note: Main fields are automatically managed by the save method on the CT_Edit_View edit screen 234 | if( ! isset( $object[$args['field_id']] ) && in_array( 'meta', $ct_table->supports ) ) { 235 | 236 | if( $field_args['multiple'] && ! $field_args['repeatable'] ) { 237 | // Delete multiple entries 238 | $meta_table_name = $ct_table->meta->db->table_name; 239 | $primary_key = $ct_table->db->primary_key; 240 | 241 | $where = array( 242 | 'meta_key' => $args['field_id'] 243 | ); 244 | 245 | $where[$primary_key] = $args['id']; 246 | 247 | return $wpdb->delete( $meta_table_name, $where ); 248 | 249 | } else { 250 | // Delete single entry 251 | return ct_delete_object_meta( $args['id'], $args['field_id'], $field->value ); 252 | } 253 | 254 | } 255 | 256 | return $check; 257 | 258 | } 259 | add_filter( 'cmb2_override_meta_remove', 'ct_cmb2_override_meta_remove', 10, 4 ); 260 | -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 9 | */ 10 | 11 | // Include the Custom Table (CT) library 12 | require_once __DIR__ . '/init.php'; 13 | 14 | /* ---------------------------------- 15 | * INITIALIZATION 16 | * Main example about how to add a custom table through CT 17 | * Safest way to start everything is through ct_init action 18 | ---------------------------------- */ 19 | 20 | function yourprefix_init() { 21 | 22 | $ct_table = ct_register_table( 'demo_logs', array( 23 | 'singular' => 'Log', 24 | 'plural' => 'Logs', 25 | 'show_ui' => true, // Make custom table visible on admin area (check 'views' parameter) 26 | 'show_in_rest' => true, // Make custom table visible on rest API 27 | //'rest_base' => 'demo-logs', // Rest base URL, if not defined will user the table name 28 | 'version' => 1, // Change the version on schema changes to run the schema auto-updater 29 | //'primary_key' => 'log_id', // If not defined will be checked on the field that hsa primary_key as true on schema 30 | 'schema' => array( 31 | 'log_id' => array( 32 | 'type' => 'bigint', 33 | 'length' => '20', 34 | 'auto_increment' => true, 35 | 'primary_key' => true, 36 | ), 37 | 'title' => array( 38 | 'type' => 'varchar', 39 | 'length' => '50', 40 | ), 41 | 'status' => array( 42 | 'type' => 'varchar', 43 | 'length' => '50', 44 | ), 45 | 'date' => array( 46 | 'type' => 'datetime', 47 | ) 48 | ), 49 | // Also you can define schema as string 50 | // 'schema' => ' 51 | // log_id bigint(20) NOT NULL AUTO_INCREMENT, 52 | // title varchar(50) NOT NULL, 53 | // status varchar(50) NOT NULL, 54 | // date datetime NOT NULL, 55 | // PRIMARY KEY (log_id) 56 | // ', 57 | // Database engine (default to InnoDB) 58 | 'engine' => 'InnoDB', 59 | // View args 60 | 'views' => array( 61 | 'add' => array( 62 | //'columns' => 1 // This will force to the add view just to one column, default is 2 63 | ), 64 | 'list' => array( 65 | // 'per_page' => 40 // This will force the per page initial value 66 | // The columns arg is a shortcut of the manage_columns and manage_sortable_columns commonly required hooks 67 | 'columns' => array( 68 | 'title' => array( 69 | 'label' => __( 'Title' ), 70 | 'sortable' => 'title', // ORDER BY title ASC 71 | ), 72 | 'status' => array( 73 | 'label' => __( 'Status' ), 74 | 'sortable' => array( 'status', false ), // ORDER BY status ASC 75 | ), 76 | 'date' => array( 77 | 'label' => __( 'Date' ), 78 | 'sortable' => array( 'date', true ), // ORDER BY date DESC 79 | ), 80 | ) 81 | ) 82 | ), 83 | 'supports' => array( 84 | 'meta', // This support automatically generates a new DB table with {table_name}_meta with a similar structure like WP post meta 85 | ) 86 | ) ); 87 | 88 | // Let's to add some demo data 89 | //$ct_table->db->insert( array( 'title' => 'Log 1' ) ); 90 | //$ct_table->db->insert( array( 'title' => 'Log 2' ) ); 91 | //$ct_table->db->insert( array( 'title' => 'Log 3' ) ); 92 | 93 | } 94 | add_action( 'ct_init', 'yourprefix_init' ); 95 | 96 | /* ---------------------------------- 97 | * LIST VIEW 98 | * Examples about some interesting hooks to use on list view 99 | ---------------------------------- */ 100 | 101 | // Columns on list view 102 | function yourprefix_manage_demo_logs_columns( $columns = array() ) { 103 | 104 | $columns['title'] = __( 'Title' ); 105 | $columns['status'] = __( 'Status' ); 106 | $columns['date'] = __( 'Date' ); 107 | 108 | // You can avoid to use this function and use an alternative way through ct_register_table() views argument like: 109 | // ct_register_table( 'demo_logs', array( 110 | // 'views' => array( 111 | // 'list' => array( 112 | // 'columns' => array( 113 | // 'title' => array( 114 | // 'label' => __( 'Title' ), 115 | // ), 116 | // 'status' => array( 117 | // 'label' => __( 'Status' ), 118 | // ), 119 | // 'date' => array( 120 | // 'label' => __( 'Date' ), 121 | // ), 122 | // ) 123 | // ) 124 | // ), 125 | // ) ); 126 | 127 | return $columns; 128 | } 129 | add_filter( 'manage_demo_logs_columns', 'yourprefix_manage_demo_logs_columns' ); 130 | 131 | // Sortable columns on list view 132 | function yourprefix_manage_demo_logs_sortable_columns( $sortable_columns = array() ) { 133 | 134 | $sortable_columns['title'] = 'title'; // ORDER BY title ASC 135 | $sortable_columns['status'] = array( 'status', false ); // ORDER BY status ASC 136 | $sortable_columns['date'] = array( 'date', true ); // ORDER BY date DESC 137 | 138 | // You can avoid to use this function and use an alternative way through ct_register_table() views argument like: 139 | // ct_register_table( 'demo_logs', array( 140 | // 'views' => array( 141 | // 'list' => array( 142 | // 'columns' => array( 143 | // 'title' => array( 144 | // 'sortable' => 'title', 145 | // ), 146 | // 'status' => array( 147 | // 'sortable' => array( 'status', false ), 148 | // ), 149 | // 'date' => array( 150 | // 'sortable' => array( 'date', true ), 151 | // ), 152 | // ) 153 | // ) 154 | // ), 155 | // ) ); 156 | 157 | return $sortable_columns; 158 | } 159 | add_filter( 'manage_demo_logs_sortable_columns', 'yourprefix_manage_demo_logs_sortable_columns' ); 160 | 161 | /* ---------------------------------- 162 | * ADD/EDIT VIEW 163 | * Examples about some interesting hooks to use on add/edit views 164 | ---------------------------------- */ 165 | 166 | // Default data when creating a new item (similar to WP auto draft) see ct_insert_object() 167 | function yourprefix_demo_logs_default_data( $default_data = array() ) { 168 | 169 | $default_data['title'] = 'Auto draft'; 170 | $default_data['status'] = 'pending'; 171 | $default_data['date'] = date( 'Y-m-d H:i:s' ); 172 | 173 | return $default_data; 174 | } 175 | add_filter( 'ct_demo_logs_default_data', 'yourprefix_demo_logs_default_data' ); 176 | 177 | // Adding meta boxes to the edit screen 178 | function yourprefix_add_meta_boxes() { 179 | 180 | add_meta_box( 181 | 'demo-meta-box-id', 182 | __( 'Demo Meta Box', 'textdomain' ), 183 | 'yourprefix_demo_meta_box_callback', 184 | 'demo_logs', 185 | 'normal' 186 | ); 187 | 188 | } 189 | add_action( 'add_meta_boxes', 'yourprefix_add_meta_boxes' ); 190 | 191 | // Meta box render callback 192 | function yourprefix_demo_meta_box_callback( $object ) { 193 | // Turn stdObject into an array 194 | $object_data = (array) $object; ?> 195 | 196 | 197 | 198 | $value ) : 199 | 200 | // Prevent display the id field 201 | if( $field === 'log_id' ) { 202 | continue; 203 | } ?> 204 | 205 | 206 | 209 | 212 | 213 | 214 | 215 | 216 |
207 | 208 | 210 | 211 |
217 | 218 | 'cmb-demo-meta-box-id', 231 | 'title' => __( 'CMB2 Demo Meta Box', 'textdomain' ), 232 | 'object_types' => array( 'demo_logs' ), 233 | ) ); 234 | 235 | $cmb->add_field( array( 236 | 'id' => 'title', 237 | 'name' => esc_html__( 'Title', 'textdomain' ), 238 | 'desc' => esc_html__( 'field description (optional)', 'textdomain' ), 239 | 'type' => 'text', 240 | ) ); 241 | 242 | $cmb->add_field( array( 243 | 'id' => 'status', 244 | 'name' => esc_html__( 'Status', 'textdomain' ), 245 | 'desc' => esc_html__( 'field description (optional)', 'textdomain' ), 246 | 'type' => 'text', 247 | ) ); 248 | 249 | // This fields just work if you defined meta as supports on ct_register_table() 250 | $cmb->add_field( array( 251 | 'id' => 'yourprefix_meta_field', 252 | 'name' => esc_html__( 'Meta field', 'textdomain' ), 253 | 'desc' => esc_html__( 'field description (optional)', 'textdomain' ), 254 | 'type' => 'text', 255 | ) ); 256 | 257 | $cmb->add_field( array( 258 | 'id' => 'yourprefix_meta_field_2', 259 | 'name' => esc_html__( 'Meta field 2', 'textdomain' ), 260 | 'desc' => esc_html__( 'field description (optional)', 'textdomain' ), 261 | 'type' => 'text', 262 | ) ); 263 | 264 | } 265 | add_action( 'cmb2_admin_init', 'yourprefix_cmb2_meta_boxes' ); 266 | 267 | /* ---------------------------------- 268 | * QUERY 269 | * As WP_Query, CT has a query class named CT_Query to apply (cached) searches on custom tables 270 | ---------------------------------- */ 271 | 272 | // Fields to apply a search, used on searches ('s' query var) 273 | function yourprefix_demo_logs_search_fields( $search_fields = array() ) { 274 | 275 | $search_fields[] = 'title'; 276 | $search_fields[] = 'status'; 277 | 278 | return $search_fields; 279 | 280 | } 281 | add_filter( 'ct_query_demo_logs_search_fields', 'yourprefix_demo_logs_search_fields' ); 282 | 283 | // Custom where, example adding support to 'log__in' and 'log__not_in' query vars 284 | function yourprefix_demo_logs_query_where( $where, $ct_query ) { 285 | 286 | global $ct_table; 287 | 288 | if( $ct_table->name !== 'demo_logs' ) { 289 | return $where; 290 | } 291 | 292 | $table_name = $ct_table->db->table_name; 293 | 294 | // Shorthand 295 | $qv = $ct_query->query_vars; 296 | 297 | // Include 298 | if( isset( $qv['log__in'] ) && ! empty( $qv['log__in'] ) ) { 299 | 300 | if( is_array( $qv['log__in'] ) ) { 301 | $include = implode( ", ", $qv['log__in'] ); 302 | } else { 303 | $include = $qv['log__in']; 304 | } 305 | 306 | if( ! empty( $include ) ) { 307 | $where .= " AND {$table_name}.log_id IN ( {$include} )"; 308 | } 309 | } 310 | 311 | // Exclude 312 | if( isset( $qv['log__not_in'] ) && ! empty( $qv['log__not_in'] ) ) { 313 | 314 | if( is_array( $qv['log__not_in'] ) ) { 315 | $exclude = implode( ", ", $qv['log__not_in'] ); 316 | } else { 317 | $exclude = $qv['log__not_in']; 318 | } 319 | 320 | if( ! empty( $exclude ) ) { 321 | $where .= " AND {$table_name}.log_id NOT IN ( {$exclude} )"; 322 | } 323 | } 324 | 325 | return $where; 326 | } 327 | add_filter( 'ct_query_where', 'yourprefix_demo_logs_query_where', 10, 2 ); 328 | 329 | /* ---------------------------------- 330 | * REST API 331 | * Examples about some interesting hooks to use on rest API 332 | ---------------------------------- */ 333 | 334 | // Register the item schema properties (used on create and update endpoints) 335 | function yourprefix_demo_logs_rest_item_schema( $schema ) { 336 | 337 | // Properties 338 | $schema['properties'] = array_merge( array( 339 | 'log_id' => array( 340 | 'description' => __( 'Unique identifier for the object.', 'textdomain' ), 341 | 'type' => 'integer', 342 | 'context' => array( 'view', 'edit', 'embed' ), 343 | ), 344 | 'title' => array( 345 | 'description' => __( 'The title for the object.', 'textdomain' ), 346 | 'type' => 'string', 347 | 'context' => array( 'view', 'edit', 'embed' ), 348 | ), 349 | 'status' => array( 350 | 'description' => __( 'Status of log for the object.', 'textdomain' ), 351 | 'type' => 'string', 352 | 'context' => array( 'view', 'edit', 'embed' ), 353 | 'readonly' => true, 354 | ), 355 | 'date' => array( 356 | 'description' => __( 'The date the object was created, in the site\'s timezone.', 'textdomain' ), 357 | 'type' => 'string', 358 | 'format' => 'date-time', 359 | 'context' => array( 'view', 'edit', 'embed' ), 360 | ), 361 | ), $schema['properties'] ); 362 | 363 | return $schema; 364 | 365 | } 366 | add_filter( 'ct_rest_demo_logs_schema', 'yourprefix_demo_logs_rest_item_schema' ); 367 | 368 | // Custom collection params, to make them work check the demo_logs_query_where() example function 369 | // Note: On this example, collection params are 'exclude' and 'include' 370 | // On demo_logs_rest_parameter_mappings() example function will be map them to the real query vars 371 | function yourprefix_demo_logs_rest_collection_params( $query_params, $ct_table ) { 372 | 373 | // Exclude 374 | $query_params['exclude'] = array( 375 | 'description' => __( 'Ensure result set excludes specific IDs.', 'textdomain' ), 376 | 'type' => 'array', 377 | 'items' => array( 378 | 'type' => 'integer', 379 | ), 380 | 'default' => array(), 381 | ); 382 | 383 | // Include 384 | $query_params['include'] = array( 385 | 'description' => __( 'Limit result set to specific IDs.', 'textdomain' ), 386 | 'type' => 'array', 387 | 'items' => array( 388 | 'type' => 'integer', 389 | ), 390 | 'default' => array(), 391 | ); 392 | 393 | 394 | return $query_params; 395 | } 396 | add_filter( 'ct_rest_demo_logs_collection_params', 'yourprefix_demo_logs_rest_collection_params', 10, 2 ); 397 | 398 | // Map custom parameters to real query var parameters (check the demo_logs_query_where() example function) 399 | function yourprefix_demo_logs_rest_parameter_mappings( $parameter_mappings, $ct_table, $request ) { 400 | 401 | $parameter_mappings['exclude'] = 'log__not_in'; 402 | $parameter_mappings['include'] = 'log__in'; 403 | 404 | return $parameter_mappings; 405 | } 406 | add_filter( 'ct_rest_demo_logs_parameter_mappings', 'yourprefix_demo_logs_rest_parameter_mappings', 10, 3 ); 407 | 408 | // Custom field sanitization on rest API updates 409 | function yourprefix_demo_logs_rest_sanitize_field_value( $value, $field, $request ) { 410 | 411 | switch( $field ) { 412 | case 'date': 413 | // Validate date. 414 | $mm = substr( $value, 5, 2 ); 415 | $jj = substr( $value, 8, 2 ); 416 | $aa = substr( $value, 0, 4 ); 417 | $valid_date = wp_checkdate( $mm, $jj, $aa, $value ); 418 | 419 | if ( ! $valid_date ) { 420 | return new WP_Error( 'rest_invalid_field', __( 'Invalid date.', 'textdomain' ), array( 'status' => 400 ) ); 421 | } 422 | break; 423 | } 424 | 425 | return $value; 426 | } 427 | add_filter( 'ct_rest_demo_logs_sanitize_field_value', 'yourprefix_demo_logs_rest_sanitize_field_value', 10, 3 ); 428 | 429 | // Register rest field 430 | function yourprefix_demo_logs_register_rest_field() { 431 | 432 | register_rest_field( 433 | 'demo_logs', 434 | 'yourprefix_meta_field', 435 | array( 436 | 'get_callback' => 'yourprefix_common_get_object_meta', 437 | 'update_callback' => 'yourprefix_common_update_object_meta', 438 | 'schema' => null, 439 | ) 440 | ); 441 | 442 | register_rest_field( 443 | 'demo_logs', 444 | 'yourprefix_meta_field_2', 445 | array( 446 | 'get_callback' => 'yourprefix_common_get_object_meta', 447 | 'update_callback' => 'yourprefix_common_update_object_meta', 448 | 'schema' => null, 449 | ) 450 | ); 451 | 452 | } 453 | add_action( 'ct_rest_api_init', 'yourprefix_demo_logs_register_rest_field' ); 454 | 455 | // Get object meta callback 456 | function yourprefix_common_get_object_meta( $object, $field_name, $request ) { 457 | return ct_get_object_meta( $object[ 'id' ], $field_name, true ); 458 | } 459 | 460 | // Update object meta callback 461 | function yourprefix_common_update_object_meta( $value, $object, $field_name ) { 462 | return ct_update_object_meta( $object[ 'id' ], $field_name, $value ); 463 | } -------------------------------------------------------------------------------- /includes/class-ct-database-schema-updater.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 6 | * 7 | * @since 1.0.0 8 | */ 9 | // Exit if accessed directly 10 | defined( 'ABSPATH' ) || exit; 11 | 12 | if ( ! class_exists( 'CT_DataBase_Schema_Updater' ) ) : 13 | 14 | class CT_DataBase_Schema_Updater { 15 | 16 | /** 17 | * @var CT_DataBase Database object 18 | */ 19 | public $ct_db; 20 | 21 | /** 22 | * @var CT_DataBase_Schema Database Schema object 23 | */ 24 | public $schema; 25 | 26 | /** 27 | * CT_DataBase_Schema_Updater constructor. 28 | * 29 | * @param CT_DataBase $ct_db 30 | */ 31 | public function __construct( $ct_db ) { 32 | 33 | $this->ct_db = $ct_db; 34 | $this->schema = $ct_db->schema; 35 | 36 | } 37 | 38 | /** 39 | * Run the database schema update 40 | * 41 | * @return bool 42 | */ 43 | public function run() { 44 | 45 | if( $this->schema ) { 46 | 47 | $alters = array(); 48 | 49 | // Get schema fields and current table definition to being compared 50 | $schema_fields = $this->schema->fields; 51 | $current_schema_fields = array(); 52 | 53 | // Get a description of current schema 54 | $schema_description = $this->ct_db->db->get_results( "DESCRIBE {$this->ct_db->table_name}" ); 55 | 56 | // Check stored schema with configured fields to check field deletions and build a custom array to be used after 57 | foreach( $schema_description as $field ) { 58 | 59 | $current_schema_fields[$field->Field] = $this->object_field_to_array( $field ); 60 | 61 | if( ! isset( $schema_fields[$field->Field] ) ) { 62 | // A field to be removed 63 | $alters[] = array( 64 | 'action' => 'DROP', 65 | 'column' => $field->Field 66 | ); 67 | } 68 | 69 | } 70 | 71 | // Check configured fields with stored fields to check field creations 72 | foreach( $schema_fields as $field_id => $field_args ) { 73 | 74 | if( ! isset( $current_schema_fields[$field_id] ) ) { 75 | // A field to be added 76 | $alters[] = array( 77 | 'action' => 'ADD', 78 | 'column' => $field_id 79 | ); 80 | 81 | } else { 82 | // Check changes in field definition 83 | 84 | // Check if key definition has changed 85 | if( $field_args['key'] !== $current_schema_fields[$field_id]['key'] ) { 86 | $alters[] = array( 87 | // Based the action on current key, if is true then ADD, if is false then DROP 88 | 'action' => ( $field_args['key'] ? 'ADD INDEX' : 'DROP INDEX' ), 89 | 'column' => $field_id 90 | ); 91 | } 92 | 93 | // TODO: Check the rest of available field args to determine was changed!!! 94 | } 95 | 96 | } 97 | 98 | // Queries to be executed at end of checks 99 | $queries = array(); 100 | 101 | foreach( $alters as $alter ) { 102 | 103 | $column = $alter['column']; 104 | 105 | switch( $alter['action'] ) { 106 | case 'ADD': 107 | $queries[] = "ALTER TABLE `{$this->ct_db->table_name}` ADD " . $this->schema->field_array_to_schema( $column, $schema_fields[$column] ) . "; "; 108 | break; 109 | case 'ADD INDEX': 110 | 111 | /* 112 | * Indexes have a maximum size of 767 bytes. WordPress 4.2 was moved to utf8mb4, which uses 4 bytes per character. 113 | * This means that an index which used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. 114 | */ 115 | $max_index_length = 191; 116 | 117 | if( $schema_fields[$column]['length'] > $max_index_length || $schema_fields[$column]['type'] === 'text' ) { 118 | $add_index_query = '`' . $column . '`(`' . $column . '`(' . $max_index_length . '))'; 119 | } else { 120 | $add_index_query = '`' . $column . '`(' . $column . '`)'; 121 | } 122 | 123 | // Prevent errors if index already exists 124 | drop_index( $this->ct_db->table_name, $column ); 125 | 126 | // For indexes query should be executed directly 127 | $this->ct_db->db->query( "ALTER TABLE `{$this->ct_db->table_name}` ADD INDEX {$add_index_query}" ); 128 | break; 129 | case 'MODIFY': 130 | $queries[] = "ALTER TABLE `{$this->ct_db->table_name}` MODIFY " . $this->schema->field_array_to_schema( $column, $schema_fields[$column] ) . "; "; 131 | break; 132 | case 'DROP': 133 | $queries[] = "ALTER TABLE `{$this->ct_db->table_name}` DROP COLUMN `{$column}`; "; 134 | 135 | // Better use a built-in function here? 136 | //maybe_drop_column( $this->ct_db->table_name, $column, "ALTER TABLE `{$this->ct_db->table_name}` DROP COLUMN {$column}" ); 137 | break; 138 | case 'DROP INDEX': 139 | // For indexes query should be executed directly 140 | //$this->ct_db->db->query( "ALTER TABLE `{$this->ct_db->table_name}` DROP INDEX {$column}" ); 141 | 142 | // Use a built-in function for safe drop 143 | drop_index( $this->ct_db->table_name, $column ); 144 | break; 145 | 146 | } 147 | } 148 | 149 | if( ! empty( $queries ) ) { 150 | 151 | // Execute the each SQL query 152 | foreach( $queries as $sql ) { 153 | $updated = $this->ct_db->db->query( $sql ); 154 | } 155 | 156 | // Was anything updated? 157 | return ! empty( $updated ); 158 | } 159 | 160 | return true; 161 | 162 | } 163 | 164 | } 165 | 166 | /** 167 | * Object field returned by the DESCRIBE sentence 168 | * 169 | * @param stdClass $field stdClass object with next keys: 170 | * - string Field Field name 171 | * - string Type Field type ("type(length) signed|unsigned") 172 | * - string Null Nullable definition ("YES"|"NO") 173 | * - string Key Key. "PRI" for primary, "MUL" for key definition ("PRI"|"MUL") 174 | * - string|NULL Default Default definition. A NULL object if not defined. ("Default value"|NULL) 175 | * - string Extra Extra definitions, "auto_increment" for example 176 | * 177 | * @return array 178 | */ 179 | public function object_field_to_array( $field ) { 180 | 181 | $field_args = array( 182 | 'type' => '', 183 | 'length' => 0, 184 | 'decimals' => 0, // numeric fields 185 | 'format' => '', // time fields 186 | 'options' => array(), // ENUM and SET types 187 | 'nullable' => (bool) ( $field->Null === 'YES' ), 188 | 'unsigned' => null, // numeric field 189 | 'zerofill' => null, // numeric field 190 | 'binary' => null, // text fields 191 | 'charset' => false, // text fields 192 | 'collate' => false, // text fields 193 | 'default' => false, 194 | 'auto_increment' => false, 195 | 'unique' => false, 196 | 'primary_key' => (bool) ( $field->Key === 'PRI' ), 197 | 'key' => (bool) ( $field->Key === 'MUL' ), 198 | ); 199 | 200 | // Determine the field type 201 | if( strpos( $field->Type, '(' ) !== false ) { 202 | // Check for "type(length)" or "type(length) signed|unsigned" 203 | 204 | $type_parts = explode( '(', $field->Type ); 205 | 206 | $field_args['type'] = $type_parts[0]; 207 | 208 | } else if( strpos( $field->Type, ' ' ) !== false ) { 209 | // Check for "type signed|unsigned" 210 | $type_parts = explode( ' ', $field->Type ); 211 | 212 | $field_args['type'] = $type_parts[0]; 213 | } 214 | 215 | $field_args['type'] = $field->Type; 216 | 217 | if( strpos( $field->Type, '(' ) !== false ) { 218 | // Check for "type(length)" or "type(length) signed|unsigned" 219 | 220 | $type_parts = explode( '(', $field->Type ); 221 | $type_part = $type_parts[1]; 222 | 223 | $type_definition_parts = explode( ')', $type_part ); 224 | $type_definition = $type_definition_parts[0]; 225 | 226 | if( ! empty( $type_definition ) ) { 227 | 228 | // Determine type definition args 229 | switch( strtoupper( $field_args['type'] ) ) { 230 | case 'ENUM': 231 | case 'SET': 232 | $field_args['options'] = explode( ',', $type_definition ); 233 | break; 234 | case 'REAL': 235 | case 'DOUBLE': 236 | case 'FLOAT': 237 | case 'DECIMAL': 238 | case 'NUMERIC': 239 | if( strpos( $type_definition, ',' ) !== false ) { 240 | $decimals = explode( ',', $type_definition ); 241 | 242 | $field_args['length'] = $decimals[0]; 243 | $field_args['decimals'] = $decimals[1]; 244 | } else if( absint( $type_definition ) !== 0 ) { 245 | $field_args['length'] = $type_definition; 246 | } 247 | break; 248 | case 'TIME': 249 | case 'TIMESTAMP': 250 | case 'DATETIME': 251 | $field_args['format'] = $type_definition; 252 | break; 253 | default: 254 | if( absint( $type_definition ) !== 0 ) { 255 | $field_args['length'] = $type_definition; 256 | } 257 | break; 258 | } 259 | 260 | } 261 | 262 | } 263 | 264 | // Check for "type signed|unsigned zerofill ..." or "type(length) signed|unsigned zerofill ..." 265 | $type_definition_parts = explode( ' ', $field->Type ); 266 | 267 | // Loop each field definition part to check extra parameters 268 | foreach( $type_definition_parts as $type_definition_part ) { 269 | 270 | if( $type_definition_part === 'unsigned' ) { 271 | $field_args['unsigned'] = true; 272 | } 273 | 274 | if( $type_definition_part === 'signed' ) { 275 | $field_args['unsigned'] = false; 276 | } 277 | 278 | if( $type_definition_part === 'zerofill' ) { 279 | $field_args['zerofill'] = true; 280 | } 281 | 282 | if( $type_definition_part === 'binary' ) { 283 | $field_args['binary'] = true; 284 | } 285 | 286 | } 287 | 288 | return $field_args; 289 | 290 | } 291 | 292 | } 293 | 294 | endif; -------------------------------------------------------------------------------- /includes/class-ct-database-schema.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 6 | * 7 | * @since 1.0.0 8 | */ 9 | // Exit if accessed directly 10 | defined( 'ABSPATH' ) || exit; 11 | 12 | if ( ! class_exists( 'CT_DataBase_Schema' ) ) : 13 | 14 | class CT_DataBase_Schema { 15 | 16 | /** 17 | * @var array Fields schema definition 18 | */ 19 | public $fields = array(); 20 | 21 | /** 22 | * @var array Keys schema definition 23 | */ 24 | public $keys = array(); 25 | 26 | public $primary_key = ''; 27 | 28 | public function __construct( $schema ) { 29 | 30 | $this->keys = array(); 31 | 32 | if( gettype( $schema ) === 'string' ) { 33 | $schema = $this->string_schema_to_array( $schema ); 34 | } 35 | 36 | if( is_array( $schema ) ) { 37 | 38 | foreach( $schema as $field_id => $field_args ) { 39 | $field_args = wp_parse_args( $field_args, array( 40 | 'type' => '', 41 | 'length' => 0, 42 | 'decimals' => 0, // numeric fields 43 | 'format' => '', // time fields 44 | 'options' => array(), // ENUM and SET types 45 | 'nullable' => false, 46 | 'unsigned' => null, // numeric field 47 | 'zerofill' => null, // numeric field 48 | 'binary' => null, // text fields 49 | 'charset' => false, // text fields 50 | 'collate' => false, // text fields 51 | 'default' => false, 52 | 'auto_increment' => false, 53 | 'unique' => false, 54 | 'primary_key' => false, 55 | 'key' => false, 56 | ) ); 57 | 58 | $this->fields[$field_id] = $field_args; 59 | } 60 | 61 | } 62 | 63 | } 64 | 65 | public function __toString() { 66 | 67 | $fields_def = array(); 68 | 69 | foreach( $this->fields as $field_id => $field_args ) { 70 | // Turn field array to schema 71 | $fields_def[] = $this->field_array_to_schema( $field_id, $field_args ); 72 | } 73 | 74 | // Setup PRIMARY KEY definition 75 | $sql = implode( ', ', $fields_def ) . ', ' 76 | . 'PRIMARY KEY (`' . $this->primary_key . '`)'; // Add two spaces to avoid issues 77 | 78 | // Setup KEY definitions 79 | if( ! empty( $this->keys ) ) { 80 | $sql .= ', ' . implode( ', ', $this->keys ); 81 | } 82 | 83 | return $sql; 84 | } 85 | 86 | /** 87 | * Convert a field definition into a schema string 88 | * 89 | * @param string $field_id 90 | * @param array $field_args 91 | * 92 | * @return string A schema string like: field type(length,decimals) UNSIGNED NOT NULL 93 | */ 94 | public function field_array_to_schema( $field_id, $field_args ) { 95 | 96 | $schema = ''; 97 | 98 | // Field name 99 | $schema .= '`' . $field_id . '` '; 100 | 101 | // Type definition 102 | $schema .= $field_args['type']; 103 | 104 | // Type definition args 105 | switch( strtoupper( $field_args['type'] ) ) { 106 | case 'ENUM': 107 | case 'SET': 108 | if( is_array( $field_args['options'] ) ) { 109 | $schema .= '(' . implode( ',', $field_args['options'] ) . ')'; 110 | } else { 111 | $schema .= '(' . $field_args['options'] . ')'; 112 | } 113 | break; 114 | case 'REAL': 115 | case 'DOUBLE': 116 | case 'FLOAT': 117 | case 'DECIMAL': 118 | case 'NUMERIC': 119 | if( $field_args['length'] !== 0 ) { 120 | $schema .= '(' . $field_args['length'] . ',' . $field_args['decimals'] . ')'; 121 | } 122 | break; 123 | case 'TIME': 124 | case 'TIMESTAMP': 125 | case 'DATETIME': 126 | if( $field_args['format'] !== '' ) { 127 | $schema .= '(' . $field_args['format'] . ')'; 128 | } 129 | break; 130 | default: 131 | if( $field_args['length'] !== 0 ) { 132 | $schema .= '(' . $field_args['length'] . ')'; 133 | } 134 | break; 135 | } 136 | 137 | $schema .= ' '; 138 | 139 | // Type specific definitions 140 | switch( strtoupper( $field_args['type'] ) ) { 141 | case 'TINYINT': 142 | case 'SMALLINT': 143 | case 'MEDIUMINT': 144 | case 'INT': 145 | case 'INTEGER': 146 | case 'BIGINT': 147 | case 'REAL': 148 | case 'DOUBLE': 149 | case 'FLOAT': 150 | case 'DECIMAL': 151 | case 'NUMERIC': 152 | // UNSIGNED definition 153 | if( $field_args['unsigned'] !== null ) { 154 | if( $field_args['unsigned'] ) { 155 | $schema .= 'UNSIGNED '; 156 | } else { 157 | $schema .= 'SIGNED '; 158 | } 159 | } 160 | 161 | // ZEROFILL definition 162 | if( $field_args['zerofill'] !== null && $field_args['zerofill'] ) { 163 | $schema .= 'ZEROFILL '; 164 | } 165 | break; 166 | case 'CHAR': 167 | case 'VARCHAR': 168 | case 'TINYTEXT': 169 | case 'TEXT': 170 | case 'MEDIUMTEXT': 171 | case 'LONGTEXT': 172 | case 'ENUM': 173 | case 'SET': 174 | // BINARY definition 175 | if( $field_args['binary'] !== null && $field_args['binary']) { 176 | $schema .= 'BINARY '; 177 | } 178 | 179 | // CHARACTER SET definition 180 | if( $field_args['charset'] !== false ) { 181 | $schema .= 'CHARACTER SET ' . $field_args['charset'] . ' '; 182 | } 183 | 184 | // COLLATE definition 185 | if( $field_args['collate'] !== false ) { 186 | $schema .= 'COLLATE ' . $field_args['collate'] . ' '; 187 | } 188 | break; 189 | } 190 | 191 | 192 | // NULL definition 193 | if( $field_args['nullable'] ) { 194 | $schema .= 'NULL '; 195 | } else { 196 | $schema .= 'NOT NULL '; 197 | } 198 | 199 | // DEFAULT definition 200 | if( $field_args['default'] !== false ) { 201 | 202 | if( gettype( $field_args['default'] ) === 'string' ) { 203 | $field_args['default'] = "'" . $field_args['default'] . "'"; 204 | } 205 | 206 | if( $field_args['default'] === null ) { 207 | $field_args['default'] = 'NULL'; 208 | } 209 | 210 | $schema .= 'DEFAULT ' . $field_args['default'] . ' '; 211 | } 212 | 213 | // UNIQUE definition 214 | if( $field_args['unique'] ) { 215 | $schema .= 'UNIQUE '; 216 | } 217 | 218 | // AUTO_INCREMENT definition 219 | if( $field_args['auto_increment'] ) { 220 | $schema .= 'AUTO_INCREMENT '; 221 | } 222 | 223 | // PRIMARY KEY definition 224 | if( $field_args['primary_key'] ) { 225 | $this->primary_key = $field_id; 226 | } 227 | 228 | // KEY definition 229 | if( $field_args['key'] ) { 230 | 231 | /* 232 | * Indexes have a maximum size of 767 bytes. WordPress 4.2 was moved to utf8mb4, which uses 4 bytes per character. 233 | * This means that an index which used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. 234 | */ 235 | $max_index_length = 191; 236 | $length = min( $field_args['length'], $max_index_length ); 237 | 238 | // Ensure that length has a value 239 | if( $length === 0 ) { 240 | $length = $max_index_length; 241 | } 242 | 243 | if( $this->is_numeric( $field_args['type'] ) || strtoupper( $field_args['type'] ) === 'DATETIME' ) { 244 | $this->keys[] = 'KEY `' . $field_id . '`(`' . $field_id . '`)'; 245 | } else { 246 | $this->keys[] = 'KEY `' . $field_id . '`(`' . $field_id . '`(' . $length . '))'; 247 | } 248 | } 249 | 250 | return $schema; 251 | 252 | } 253 | 254 | /** 255 | * Convert a schema string into an array of fields definitions 256 | * 257 | * @param string $schema 258 | * 259 | * @return array 260 | */ 261 | public function string_schema_to_array( $schema ) { 262 | 263 | $schema_array = explode( ',', trim( $schema ) ); 264 | $new_schema = array(); 265 | 266 | foreach( $schema_array as $field_def ) { 267 | 268 | $field_id = ''; 269 | $field_args = array(); 270 | 271 | $field_def_parts = explode( ' ', trim( $field_def ) ); 272 | 273 | foreach( $field_def_parts as $index => $field_def_part ) { 274 | 275 | 276 | if( $index === 0 ) { 277 | 278 | // Field id at index 0 279 | if( $field_def_part !== 'PRIMARY' && $field_def_part !== 'KEY' ) { 280 | $field_id = $field_def_part; 281 | continue; 282 | } 283 | 284 | // PRIMARY KEY at index 0 285 | if( $field_def_part === 'PRIMARY' ) { 286 | if( isset( $field_def_parts[$index+1] ) && strtoupper( $field_def_parts[$index+1] ) === 'KEY' && isset( $field_def_parts[$index+2] ) ) { 287 | $primary_key_field = str_replace( array( '(', ')' ), '', $field_def_parts[$index+2] ); 288 | 289 | if( isset( $this->fields[$primary_key_field] ) ) { 290 | $this->fields[$primary_key_field]['primary_key'] = true; 291 | continue; 292 | } 293 | } 294 | } 295 | } 296 | 297 | // NOT NULL definition 298 | if( strtoupper( $field_def_part ) === 'NOT' ) { 299 | if( isset( $field_def_parts[$index+1] ) && strtoupper( $field_def_parts[$index+1] ) === 'NULL' ) { 300 | $field_args['nullable'] = false; 301 | continue; 302 | } 303 | } 304 | 305 | // NULL definition 306 | if( strtoupper( $field_def_part ) === 'NULL' ) { 307 | if( isset( $field_def_parts[$index-1] ) && strtoupper( $field_def_parts[$index-1] ) !== 'NOT' ) { 308 | $field_args['nullable'] = true; 309 | continue; 310 | } 311 | } 312 | 313 | // UNSIGNED definition 314 | if( strtoupper( $field_def_part ) === 'UNSIGNED' ) { 315 | $field_args['unsigned'] = true; 316 | continue; 317 | } 318 | 319 | // SIGNED definition 320 | if( strtoupper( $field_def_part ) === 'SIGNED' ) { 321 | $field_args['unsigned'] = false; 322 | continue; 323 | } 324 | 325 | // ZEROFILL definition 326 | if( strtoupper( $field_def_part ) === 'ZEROFILL' ) { 327 | $field_args['zerofill'] = true; 328 | continue; 329 | } 330 | 331 | // BINARY definition 332 | if( strtoupper( $field_def_part ) === 'BINARY' ) { 333 | $field_args['binary'] = true; 334 | continue; 335 | } 336 | 337 | // CHARACTER SET definition 338 | if( strtoupper( $field_def_part ) === 'CHARACTER' ) { 339 | if( isset( $field_def_parts[$index+1] ) && strtoupper( $field_def_parts[$index+1] ) === 'SET' ) { 340 | $field_args['charset'] = $field_def_parts[$index+2]; 341 | continue; 342 | } 343 | } 344 | 345 | // COLLATE definition 346 | if( strtoupper( $field_def_part ) === 'COLLATE' ) { 347 | if( isset( $field_def_parts[$index+1] ) ) { 348 | $field_args['collate'] = $field_def_parts[$index+1]; 349 | continue; 350 | } 351 | } 352 | 353 | // DEFAULT definition 354 | if( strtoupper( $field_def_part ) === 'DEFAULT' ) { 355 | if( isset( $field_def_parts[$index+1] ) ) { 356 | $field_args['default'] = $field_def_parts[$index+1]; 357 | continue; 358 | } 359 | } 360 | 361 | // UNIQUE definition 362 | if( strtoupper( $field_def_part ) === 'UNIQUE' ) { 363 | $field_args['unique'] = true; 364 | continue; 365 | } 366 | 367 | // AUTO_INCREMENT definition 368 | if( strtoupper( $field_def_part ) === 'AUTO_INCREMENT' ) { 369 | $field_args['auto_increment'] = true; 370 | continue; 371 | } 372 | 373 | // PRIMARY KEY definition 374 | if( strtoupper( $field_def_part ) === 'PRIMARY' ) { 375 | if( isset( $field_def_parts[$index+1] ) && strtoupper( $field_def_parts[$index+1] ) === 'KEY' ) { 376 | $field_args['primary_key'] = true; 377 | continue; 378 | } 379 | } 380 | 381 | $type_parts = explode( '(', $field_def_part ); 382 | 383 | // Possible field type 384 | if( in_array( strtoupper( $field_def_part ), $this->allowed_field_types() ) ) { 385 | $field_args['type'] = strtoupper( $field_def_part ); 386 | continue; 387 | } else if( isset( $type_parts[0] ) && in_array( strtoupper( $type_parts[0] ), $this->allowed_field_types() ) ) { 388 | $field_args['type'] = strtoupper( $type_parts[0] ); 389 | 390 | if( isset( $type_parts[1] ) ) { 391 | $type_def = explode( ',', str_replace(')', '', $type_parts[1] ) ); 392 | 393 | switch( $field_args['type'] ) { 394 | case 'ENUM': 395 | case 'SET': 396 | $field_args['options'] = $type_def; 397 | break; 398 | case 'REAL': 399 | case 'DOUBLE': 400 | case 'FLOAT': 401 | case 'DECIMAL': 402 | case 'NUMERIC': 403 | $field_args['length'] = $type_def[0]; 404 | 405 | if( isset( $type_def[1] ) ) { 406 | $field_args['decimals'] = $type_def[1]; 407 | } 408 | break; 409 | case 'TIME': 410 | case 'TIMESTAMP': 411 | case 'DATETIME': 412 | $field_args['format'] = $type_def[0]; 413 | break; 414 | default: 415 | $field_args['length'] = $type_def[0]; 416 | break; 417 | } 418 | } 419 | } 420 | } 421 | 422 | if( ! empty( $field_id ) && ! empty( $field_args ) ) { 423 | $new_schema[$field_id] = $field_args; 424 | } 425 | } 426 | 427 | return $new_schema; 428 | 429 | } 430 | 431 | /** 432 | * Get the list of allowed types 433 | * 434 | * @return array 435 | */ 436 | public function allowed_field_types() { 437 | return array( 438 | 'BIT', 439 | 'TINYINT', 440 | 'SMALLINT', 441 | 'MEDIUMINT', 442 | 'INT', 443 | 'INTEGER', 444 | 'BIGINT', 445 | 'REAL', 446 | 'DOUBLE', 447 | 'FLOAT', 448 | 'DECIMAL', 449 | 'NUMERIC', 450 | 'DATE', 451 | 'TIME', 452 | 'TIMESTAMP', 453 | 'DATETIME', 454 | 'YEAR', 455 | 'CHAR', 456 | 'VARCHAR', 457 | 'BINARY', 458 | 'VARBINARY', 459 | 'TINYBLOB', 460 | 'BLOB', 461 | 'MEDIUMBLOB', 462 | 'LONGBLOB', 463 | 'TINYTEXT', 464 | 'TEXT', 465 | 'JSON' 466 | ); 467 | } 468 | 469 | /** 470 | * Check if given type is numeric 471 | * 472 | * @param string $type 473 | * 474 | * @return bool 475 | */ 476 | public function is_numeric( $type ) { 477 | 478 | return in_array( strtoupper( $type ), array( 479 | 'TINYINT', 480 | 'SMALLINT', 481 | 'MEDIUMINT', 482 | 'INT', 483 | 'INTEGER', 484 | 'BIGINT', 485 | 'REAL', 486 | 'DOUBLE', 487 | 'FLOAT', 488 | 'DECIMAL', 489 | 'NUMERIC', 490 | ) ); 491 | 492 | } 493 | 494 | } 495 | 496 | endif; -------------------------------------------------------------------------------- /includes/class-ct-database.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 8 | * 9 | * @since 1.0.0 10 | */ 11 | // Exit if accessed directly 12 | defined( 'ABSPATH' ) || exit; 13 | 14 | if ( ! class_exists( 'CT_DataBase' ) ) : 15 | 16 | class CT_DataBase { 17 | 18 | /** 19 | * @var string Table name, without the global table prefix 20 | */ 21 | protected $name = ''; 22 | 23 | /** 24 | * @var string Table primary key 25 | */ 26 | public $primary_key = ''; 27 | 28 | /** 29 | * @var int Database version 30 | */ 31 | protected $version = 0; 32 | 33 | /** 34 | * @var boolean Is this table for a site, or global 35 | */ 36 | public $global = false; 37 | 38 | /** 39 | * @var string Database version key (saved in _options or _sitemeta) 40 | */ 41 | protected $db_version_key = ''; 42 | 43 | /** 44 | * @var string Current database version 45 | */ 46 | protected $db_version = 0; 47 | 48 | /** 49 | * @var string Table name 50 | */ 51 | public $table_name = ''; 52 | 53 | /** 54 | * @var CT_DataBase_Schema Table schema 55 | */ 56 | public $schema = ''; 57 | 58 | /** 59 | * @var string Database engine for table (default InnoDB) 60 | */ 61 | public $engine = ''; 62 | 63 | /** 64 | * @var string Database character-set & collation for table 65 | */ 66 | public $charset_collation = ''; 67 | 68 | /** 69 | * @var WPDB Database object (usually $GLOBALS['wpdb']) 70 | */ 71 | public $db = false; 72 | 73 | /** 74 | * @var string Used to meet if database is in a group of tables to reduce database exists query calls 75 | */ 76 | public $group = ''; 77 | 78 | /** 79 | * @var bool Stores if database has been found to reduce query calls 80 | */ 81 | protected $exists = false; 82 | 83 | /** Methods ***************************************************************/ 84 | 85 | /** 86 | * Hook into queries, admin screens, and more! 87 | * 88 | * @since 1.0.0 89 | */ 90 | public function __construct( $name, $args ) { 91 | 92 | $this->name = $name; 93 | 94 | $this->primary_key = ( isset( $args['primary_key'] ) ) ? $args['primary_key'] : ''; 95 | 96 | $this->version = ( isset( $args['version'] ) ) ? $args['version'] : 1; 97 | 98 | $this->global = ( isset( $args['global'] ) && $args['global'] === true ) ? true : false; 99 | 100 | $this->schema = ( isset( $args['schema'] ) ) ? new CT_DataBase_Schema( $args['schema'] ) : ''; 101 | 102 | $this->group = ( isset( $args['group'] ) ) ? $args['group'] : strtok( $this->name, '_'); 103 | 104 | // If not primary key given, then look at out schema 105 | if( $this->schema && ! $this->primary_key ) { 106 | foreach( $this->schema->fields as $field_id => $field_args ) { 107 | if( $field_args['primary_key'] === true ) { 108 | $this->primary_key = $field_id; 109 | break; 110 | } 111 | } 112 | } 113 | 114 | $this->engine = ( isset( $args['engine'] ) ) ? $args['engine'] : 'InnoDB'; 115 | 116 | // Bail if no database object or table name 117 | if ( empty( $GLOBALS['wpdb'] ) || empty( $this->name ) ) { 118 | return; 119 | } 120 | 121 | // Setup the database 122 | $this->set_db(); 123 | 124 | // Get the version of he table currently in the database 125 | $this->get_db_version(); 126 | 127 | // Add the table to the object 128 | $this->set_wpdb_tables(); 129 | 130 | // Setup the database schema 131 | $this->set_schema(); 132 | 133 | // Add hooks to WordPress actions 134 | $this->add_hooks(); 135 | } 136 | 137 | /** Abstract **************************************************************/ 138 | 139 | /** 140 | * Setup this database table 141 | * 142 | * @since 1.0.0 143 | */ 144 | protected function set_schema() { 145 | 146 | } 147 | 148 | /** 149 | * Upgrade this database table 150 | * 151 | * @since 1.0.0 152 | */ 153 | protected function upgrade() { 154 | 155 | $schema_updater = new CT_DataBase_Schema_Updater( $this ); 156 | 157 | $schema_updater->run(); 158 | 159 | } 160 | 161 | /** Public ****************************************************************/ 162 | 163 | /** 164 | * Update table version & references. 165 | * 166 | * Hooked to the "switch_blog" action. 167 | * 168 | * @since 1.0.0 169 | * 170 | * @param int $site_id 171 | */ 172 | public function switch_blog( $site_id = 0 ) { 173 | 174 | // Update DB version based on the current site 175 | if ( false === $this->global ) { 176 | $this->db_version = get_blog_option( $site_id, $this->db_version_key, false ); 177 | } 178 | 179 | // Update table references based on th current site 180 | $this->set_wpdb_tables(); 181 | } 182 | 183 | /** 184 | * Maybe upgrade the database table. Handles creation & schema changes. 185 | * 186 | * Hooked to the "admin_init" action. 187 | * 188 | * @since 1.0.0 189 | */ 190 | public function maybe_upgrade() { 191 | 192 | // Bail if no upgrade needed 193 | if ( version_compare( (int) $this->db_version, (int) $this->version, '>=' ) && $this->exists() ) { 194 | return; 195 | } 196 | 197 | // Include file with dbDelta() for create/upgrade usages 198 | if ( ! function_exists( 'dbDelta' ) ) { 199 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 200 | } 201 | 202 | // Bail if global and upgrading global tables is not allowed 203 | if ( ( true === $this->global ) && ! wp_should_upgrade_global_tables() ) { 204 | return; 205 | } 206 | 207 | // Create or upgrade? 208 | $this->exists() 209 | ? $this->upgrade() 210 | : $this->create(); 211 | 212 | // Set the database version 213 | if ( $this->exists() ) { 214 | $this->set_db_version(); 215 | } 216 | } 217 | 218 | public function get( $id ) { 219 | 220 | return $this->db->get_row( $this->db->prepare( "SELECT * FROM {$this->table_name} WHERE {$this->primary_key} = %s", $id ) ); 221 | 222 | } 223 | 224 | public function query( $args = array(), $output = OBJECT ) { 225 | 226 | return $this->db->get_results( "SELECT * FROM {$this->table_name}" ); 227 | 228 | } 229 | 230 | public function insert( $data ) { 231 | 232 | if( $this->db->insert( $this->table_name, $data ) ) { 233 | return $this->db->insert_id; 234 | } 235 | 236 | return false; 237 | 238 | } 239 | 240 | public function update( $data, $where ) { 241 | 242 | $table_data = array(); 243 | $schema_fields = array_keys( $this->schema->fields ); 244 | 245 | // Filter extra data to prevent insert data outside table schema 246 | foreach( $data as $field => $value ) { 247 | if( ! in_array( $field, $schema_fields ) ) { 248 | continue; 249 | } 250 | 251 | $table_data[$field] = $value; 252 | } 253 | 254 | return $this->db->update( $this->table_name, $table_data, $where ); 255 | 256 | } 257 | 258 | public function delete( $value ) { 259 | 260 | return $this->db->query( $this->db->prepare( "DELETE FROM {$this->table_name} WHERE {$this->primary_key} = %s", $value ) ); 261 | 262 | } 263 | 264 | /** Private ***************************************************************/ 265 | 266 | /** 267 | * Setup the necessary WPDB variables 268 | * 269 | * @since 1.0.0 270 | */ 271 | private function set_db() { 272 | 273 | // Setup database 274 | $this->db = $GLOBALS['wpdb']; 275 | $this->name = sanitize_key( $this->name ); 276 | 277 | // Maybe create database key 278 | if ( empty( $this->db_version_key ) ) { 279 | $this->db_version_key = "wpdb_{$this->name}_version"; 280 | } 281 | } 282 | 283 | /** 284 | * Modify the database object and add the table to it 285 | * 286 | * This is necessary to do directly because WordPress does have a mechanism 287 | * for manipulating them safely. It's pretty fragile, but oh well. 288 | * 289 | * @since 1.0.0 290 | */ 291 | private function set_wpdb_tables() { 292 | 293 | // Global 294 | if ( true === $this->global ) { 295 | $prefix = $this->db->get_blog_prefix( 0 ); 296 | $this->db->{$this->name} = "{$prefix}{$this->name}"; 297 | $this->db->ms_global_tables[] = $this->name; 298 | 299 | // Site 300 | } else { 301 | $prefix = $this->db->get_blog_prefix( null ); 302 | $this->db->{$this->name} = "{$prefix}{$this->name}"; 303 | $this->db->tables[] = $this->name; 304 | } 305 | 306 | // Set the table name locally 307 | $this->table_name = $this->db->{$this->name}; 308 | 309 | // Charset 310 | if ( ! empty( $this->db->charset ) ) { 311 | $this->charset_collation = "DEFAULT CHARACTER SET {$this->db->charset}"; 312 | } 313 | 314 | // Collation 315 | if ( ! empty( $this->db->collate ) ) { 316 | $this->charset_collation .= " COLLATE {$this->db->collate}"; 317 | } 318 | } 319 | 320 | /** 321 | * Set the database version to the table version. 322 | * 323 | * Saves global table version to "wp_sitemeta" to the main network 324 | * 325 | * @since 1.0.0 326 | */ 327 | private function set_db_version() { 328 | 329 | // Set the class version 330 | $this->db_version = $this->version; 331 | 332 | // Update the DB version 333 | ( true === $this->global ) 334 | ? update_network_option( null, $this->db_version_key, $this->version ) 335 | : update_option( $this->db_version_key, $this->version ); 336 | } 337 | 338 | /** 339 | * Get the table version from the database. 340 | * 341 | * Gets global table version from "wp_sitemeta" to the main network 342 | * 343 | * @since 1.0.0 344 | */ 345 | private function get_db_version() { 346 | $this->db_version = ( true === $this->global ) 347 | ? get_network_option( null, $this->db_version_key, false ) 348 | : get_option( $this->db_version_key, false ); 349 | } 350 | 351 | /** 352 | * Add class hooks to WordPress actions 353 | * 354 | * @since 1.0.0 355 | */ 356 | private function add_hooks() { 357 | 358 | // Activation hook 359 | register_activation_hook( __FILE__, array( $this, 'maybe_upgrade' ) ); 360 | 361 | // Add table to the global database object 362 | add_action( 'switch_blog', array( $this, 'switch_blog' ) ); 363 | add_action( 'admin_init', array( $this, 'maybe_upgrade' ) ); 364 | } 365 | 366 | /** 367 | * Create the table 368 | * 369 | * @since 1.0.0 370 | */ 371 | private function create() { 372 | 373 | // Run CREATE TABLE query 374 | $created = dbDelta( "CREATE TABLE `{$this->table_name}` ( {$this->schema} ) ENGINE={$this->engine} {$this->charset_collation};" ); 375 | 376 | // Was anything created? 377 | $this->exists = ! empty( $created ); 378 | 379 | // Add the table to the group when created 380 | if( $this->exists && $this->group !== '' ) { 381 | ct_add_table_to_group( $this->group, $this->table_name ); 382 | } 383 | 384 | return ! empty( $created ); 385 | } 386 | 387 | /** 388 | * Check if table already exists 389 | * 390 | * @since 1.0.0 391 | * 392 | * @return bool 393 | */ 394 | public function exists() { 395 | 396 | if( $this->exists === true ) { 397 | return $this->exists; 398 | } 399 | 400 | if( $this->group === '' ) { 401 | // Table not in group 402 | $table_exist = $this->db->get_var( $this->db->prepare( 403 | "SHOW TABLES LIKE %s", 404 | $this->db->esc_like( $this->table_name ) 405 | ) ); 406 | 407 | $this->exists = ! empty( $table_exist ); 408 | } else { 409 | // Table in group 410 | $tables = ct_get_tables_in_group( $this->group ); 411 | 412 | $this->exists = in_array( $this->table_name, $tables ); 413 | } 414 | 415 | return $this->exists; 416 | 417 | } 418 | 419 | } 420 | endif; -------------------------------------------------------------------------------- /includes/class-ct-edit-view.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 6 | * 7 | * @since 1.0.0 8 | */ 9 | // Exit if accessed directly 10 | defined( 'ABSPATH' ) || exit; 11 | 12 | if ( ! class_exists( 'CT_Edit_View' ) ) : 13 | 14 | class CT_Edit_View extends CT_View { 15 | 16 | protected $object_id = 0; 17 | protected $object = null; 18 | protected $editing = false; 19 | protected $message = false; 20 | protected $columns = 2; 21 | 22 | public function __construct( $name, $args ) { 23 | 24 | parent::__construct( $name, $args ); 25 | 26 | $this->columns = isset( $args['columns'] ) ? $args['columns'] : 2; 27 | 28 | } 29 | 30 | public function init() { 31 | 32 | global $ct_registered_tables, $ct_table; 33 | 34 | if( ! isset( $ct_registered_tables[$this->name] ) ) { 35 | wp_die( __( 'Invalid item type.' ) ); 36 | } 37 | 38 | // Setup global $ct_table 39 | $ct_table = $ct_registered_tables[$this->name]; 40 | 41 | // If not CT object, die 42 | if ( ! $ct_table ) 43 | wp_die( __( 'Invalid item type.' ) ); 44 | 45 | // If not CT object allow ui, die 46 | if ( ! $ct_table->show_ui ) { 47 | wp_die( __( 'Sorry, you are not allowed to edit items of this type.' ) ); 48 | } 49 | 50 | if( isset( $_POST['ct-save'] ) ) { 51 | // Saving 52 | $this->save(); 53 | } 54 | 55 | $primary_key = $ct_table->db->primary_key; 56 | 57 | if( isset( $_GET[$primary_key] ) ) { 58 | // Editing object 59 | $this->object_id = (int) $_GET[$primary_key]; 60 | $this->object = $ct_table->db->get( $this->object_id ); 61 | $this->editing = true; 62 | 63 | // If not id, return to list 64 | if ( empty( $this->object_id ) ) { 65 | wp_redirect( ct_get_list_link( $ct_table->name ) ); 66 | exit(); 67 | } 68 | 69 | // If not object, die 70 | if ( ! $this->object ) { 71 | wp_die( __( 'You attempted to edit an item that doesn’t exist. Perhaps it was deleted?' ) ); 72 | } 73 | 74 | // If not current user can edit, die 75 | if ( ! current_user_can( $ct_table->cap->edit_item, $this->object_id ) ) { 76 | wp_die( __( 'Sorry, you are not allowed to edit this item.' ) ); 77 | } 78 | 79 | } else { 80 | // See filter "ct_{$ct_table->name}_default_data" 81 | $this->object_id = ct_insert_object( array() ); 82 | 83 | // If not id, return to list 84 | if ( empty( $this->object_id ) ) { 85 | wp_redirect( ct_get_list_link( $ct_table->name ) ); 86 | exit(); 87 | } 88 | 89 | $this->object = ct_get_object( $this->object_id ); 90 | 91 | // If not object, die 92 | if ( ! $this->object ) 93 | wp_die( __( 'Unable to create the draft item.' ) ); 94 | 95 | // If not current user can create, die 96 | if ( ! current_user_can( $ct_table->cap->create_items, $this->object_id ) 97 | && ! current_user_can( $ct_table->cap->edit_item, $this->object_id ) ) { 98 | wp_die( __( 'Sorry, you are not allowed to create items of this type.' ) ); 99 | } 100 | 101 | // Redirect to edit screen to prevent add a draft item multiples times 102 | wp_redirect( ct_get_edit_link( $ct_table->name, $this->object_id ) ); 103 | } 104 | 105 | } 106 | 107 | /** 108 | * Screen settings text displayed in the Screen Options tab. 109 | * 110 | * @param string $screen_settings Screen settings. 111 | * @param WP_Screen $screen WP_Screen object. 112 | */ 113 | public function screen_settings( $screen_settings, $screen ) { 114 | 115 | $this->render_meta_boxes_preferences(); 116 | $this->render_screen_layout(); 117 | 118 | } 119 | 120 | /** 121 | * Render the meta boxes preferences. 122 | * 123 | * @since 1.0.0 124 | * 125 | * @global array $wp_meta_boxes 126 | */ 127 | public function render_meta_boxes_preferences() { 128 | 129 | global $wp_meta_boxes, $ct_table; 130 | 131 | // TODO: Forced to place it here until fix issue with load-{pagehook} 132 | 133 | /** This action is documented in wp-admin/edit-form-advanced.php */ 134 | do_action( 'add_meta_boxes', $ct_table->name, $this->object ); 135 | 136 | /** This action is documented in wp-admin/edit-form-advanced.php */ 137 | do_action( "add_meta_boxes_{$ct_table->name}", $this->object ); 138 | 139 | /** This action is documented in wp-admin/edit-form-advanced.php */ 140 | do_action( 'do_meta_boxes', $ct_table->name, 'normal', $this->object ); 141 | /** This action is documented in wp-admin/edit-form-advanced.php */ 142 | do_action( 'do_meta_boxes', $ct_table->name, 'advanced', $this->object ); 143 | /** This action is documented in wp-admin/edit-form-advanced.php */ 144 | do_action( 'do_meta_boxes', $ct_table->name, 'side', $this->object ); 145 | 146 | if ( ! isset( $wp_meta_boxes[ $ct_table->name ] ) ) { 147 | return; 148 | } 149 | ?> 150 | 151 |
152 | 153 | name ); ?> 154 |
155 | 156 | columns <= 1 ) { 167 | return; 168 | } 169 | 170 | $screen_layout_columns = get_current_screen()->get_columns(); 171 | 172 | if( ! $screen_layout_columns ) { 173 | $screen_layout_columns = $this->columns; 174 | } 175 | 176 | $num = $this->columns; 177 | 178 | ?> 179 |
180 | 183 | 188 | 190 |
191 | name}_edit_screen_submit_meta_box_top", $object, $ct_table, $this->editing, $this ); 209 | 210 | $submit_label = __( 'Add' ); 211 | 212 | if( $this->editing ) { 213 | $submit_label = __( 'Update' ); 214 | } 215 | 216 | /** 217 | * Filter to override the submit button label. 218 | * 219 | * @since 1.0.0 220 | * 221 | * @param string $submit_label The submit label. 222 | * @param object $object Object. 223 | * @param CT_Table $ct_table CT Table object. 224 | * @param bool $editing True if edit screen, false if is adding a new one. 225 | * @param CT_Edit_View $view Edit view object. 226 | */ 227 | $submit_label = apply_filters( "ct_{$ct_table->name}_edit_screen_submit_label", $submit_label, $object, $ct_table, $this->editing, $this ); 228 | 229 | $primary_key = $ct_table->db->primary_key; 230 | $object_id = $object->$primary_key; 231 | 232 | ?> 233 | 234 |
235 | 236 | name}_edit_screen_submit_meta_box_submit_post_top", $object, $ct_table, $this->editing, $this ); ?> 248 | 249 |
250 | 251 | name}_edit_screen_submit_meta_box_minor_publishing_actions", $object, $ct_table, $this->editing, $this ); 263 | $minor_publishing_actions = ob_get_clean(); ?> 264 | 265 | 267 |
268 | 269 | 270 | name}_edit_screen_submit_meta_box_misc_publishing_actions", $object, $ct_table, $this->editing, $this ); 282 | $misc_publishing_actions = ob_get_clean(); ?> 283 | 284 | 286 |
287 | 288 | 289 |
290 | 291 |
292 | 293 |
294 | 295 | cap->delete_item, $object_id ) ) { 297 | 298 | printf( 299 | '%s', 300 | ct_get_delete_link( $ct_table->name, $object_id ), 301 | "return confirm('" . 302 | esc_attr( __( "Are you sure you want to delete this item?\\n\\nClick \\'Cancel\\' to go back, \\'OK\\' to confirm the delete." ) ) . 303 | "');", 304 | esc_attr( __( 'Delete permanently' ) ), 305 | __( 'Delete Permanently' ) 306 | ); 307 | 308 | } ?> 309 | 310 |
311 | 312 | 313 |
314 | 315 |
316 | 317 |
318 | 319 | name}_edit_screen_submit_meta_box_submit_post_bottom", $object, $ct_table, $this->editing, $this ); ?> 331 | 332 |
333 | 334 | name}_edit_screen_submit_meta_box_bottom", $object, $ct_table, $this->editing, $this ); 347 | } 348 | 349 | public function save() { 350 | 351 | global $ct_registered_tables, $ct_table; 352 | 353 | // If not CT object, die 354 | if ( ! $ct_table ) 355 | wp_die( __( 'Invalid item type.' ) ); 356 | 357 | // If not CT object allow ui, die 358 | if ( ! $ct_table->show_ui ) { 359 | wp_die( __( 'Sorry, you are not allowed to edit items of this type.' ) ); 360 | } 361 | 362 | $primary_key = $ct_table->db->primary_key; 363 | 364 | if( ! isset( $_POST[$primary_key] ) ) { 365 | wp_die( __( 'Invalid item type.' ) ); 366 | } 367 | 368 | $object_id = $_POST[$primary_key]; 369 | 370 | // Nonce check 371 | if ( ! isset( $_REQUEST['_wpnonce'] ) ) { 372 | wp_die( __( 'Sorry, you are not allowed to edit this item.' ) ); 373 | } 374 | 375 | if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'ct_edit_' . $object_id ) ) { 376 | wp_die( __( 'Sorry, you are not allowed to edit this item.' ) ); 377 | } 378 | 379 | $object_data = &$_POST; 380 | 381 | unset( $object_data['ct-save'] ); 382 | 383 | $success = ct_update_object( $object_data ); 384 | 385 | $location = add_query_arg( array( $primary_key => $object_id ), $this->get_link() ); 386 | 387 | if( $success ) { 388 | $location = add_query_arg( array( 'message' => 1 ), $location ); 389 | } else { 390 | $location = add_query_arg( array( 'message' => 0 ), $location ); 391 | } 392 | 393 | wp_redirect( $location ); 394 | exit; 395 | 396 | } 397 | 398 | public function pre_render() { 399 | 400 | global $ct_registered_tables, $ct_table; 401 | 402 | $messages = array( 403 | 0 => __( '%s could not be updated.' ), 404 | 1 => __( '%s updated successfully.' ), 405 | ); 406 | 407 | /** 408 | * Filters the table updated messages (string keys allowed!). 409 | * 410 | * @since 1.0.0 411 | * 412 | * @param array $messages Post updated messages. For defaults @see $messages declarations above. 413 | */ 414 | $messages = apply_filters( 'ct_table_updated_messages', $messages ); 415 | 416 | // Setup screen message 417 | if ( isset($_GET['message']) ) { 418 | 419 | if ( isset($messages[$_GET['message']]) ) 420 | $this->message = sprintf( $messages[$_GET['message']], $ct_table->labels->singular_name ); 421 | 422 | } 423 | 424 | wp_enqueue_script( 'post' ); 425 | 426 | if ( wp_is_mobile() ) { 427 | wp_enqueue_script( 'jquery-touch-punch' ); 428 | } 429 | 430 | // Register submitdiv metabox 431 | add_meta_box( 'submitdiv', __( 'Save Changes' ), array( $this, 'submit_meta_box' ), $ct_table->name, 'side', 'core' ); 432 | 433 | /** 434 | * Fires after all built-in meta boxes have been added. 435 | * 436 | * @since 1.0.0 437 | * 438 | * @param string $post_type Post type. 439 | * @param WP_Post $post Post object. 440 | */ 441 | do_action( 'add_meta_boxes', $ct_table->name, $this->object ); 442 | 443 | /** 444 | * Fires after all built-in meta boxes have been added, contextually for the given post type. 445 | * 446 | * The dynamic portion of the hook, `$post_type`, refers to the post type of the post. 447 | * 448 | * @since 1.0.0 449 | * 450 | * @param WP_Post $post Post object. 451 | */ 452 | do_action( "add_meta_boxes_{$ct_table->name}", $this->object ); 453 | 454 | /** 455 | * Fires after meta boxes have been added. 456 | * 457 | * Fires once for each of the default meta box contexts: normal, advanced, and side. 458 | * 459 | * @since 1.0.0 460 | * 461 | * @param string $post_type Type of the object. 462 | * @param string $context string Meta box context. 463 | * @param WP_Post $object The object. 464 | */ 465 | do_action( 'do_meta_boxes', $ct_table->name, 'normal', $this->object ); 466 | /** This action is documented in wp-admin/edit-form-advanced.php */ 467 | do_action( 'do_meta_boxes', $ct_table->name, 'advanced', $this->object ); 468 | /** This action is documented in wp-admin/edit-form-advanced.php */ 469 | do_action( 'do_meta_boxes', $ct_table->name, 'side', $this->object ); 470 | 471 | // TODO: Need to add it manually through screen_settings() function 472 | //add_screen_option( 'layout_columns', array( 'max' => $this->columns, 'default' => $this->columns ) ); 473 | 474 | } 475 | 476 | public function render() { 477 | 478 | global $ct_registered_tables, $ct_table; 479 | 480 | $this->pre_render(); 481 | 482 | if( $this->editing ) { 483 | $title = $ct_table->labels->edit_item; 484 | $new_url = ( $ct_table->views->add ? $ct_table->views->add->get_link() : false ); 485 | } else { 486 | $title = $ct_table->labels->add_new_item; 487 | } 488 | 489 | ?> 490 | 491 |
492 | 493 |

494 | 495 | cap->create_items ) ) : 496 | echo ' ' . esc_html( $ct_table->labels->add_new_item ) . ''; 497 | endif; ?> 498 | 499 |
500 | 501 | message ) : ?> 502 |

message; ?>

503 | 504 | 505 |
506 | 507 | 508 | object_id ); ?> 509 | 510 | object ); ?> 521 | 522 |
523 | 524 |
525 | 526 |
527 | 528 | name, 'side', $this->object ); ?> 529 | 530 |
531 | 532 |
533 | 534 | name, 'normal', $this->object ); ?> 535 | 536 | name, 'advanced', $this->object ); ?> 537 | 538 |
539 | 540 |
541 | 542 |
543 | 544 |
545 | 546 | object ); ?> 555 | 556 |
557 | 558 |
559 | 560 | , Ruben Garcia 8 | * 9 | * @since 1.0.0 10 | */ 11 | // Exit if accessed directly 12 | defined( 'ABSPATH' ) || exit; 13 | 14 | if ( ! class_exists( 'CT_List_Table' ) ) : 15 | 16 | class CT_List_Table extends WP_List_Table { 17 | 18 | /** 19 | * Get things started 20 | * 21 | * @access public 22 | * @since 1.0.0 23 | * 24 | * @param array $args Optional. Arbitrary display and query arguments to pass through 25 | * the list table. Default empty array. 26 | */ 27 | public function __construct( $args = array() ) { 28 | global $ct_table; 29 | 30 | parent::__construct( array( 31 | 'singular' => $ct_table->labels->singular_name, 32 | 'plural' => $ct_table->labels->plural_name, 33 | 'screen' => convert_to_screen( $ct_table->labels->plural_name ) 34 | ) ); 35 | } 36 | 37 | /** 38 | * Show the search field 39 | * 40 | * @since 1.0.0 41 | * 42 | * @param string $text Label for the search box 43 | * @param string $input_id ID of the search box 44 | * 45 | * @return void 46 | */ 47 | public function search_box( $text, $input_id ) { 48 | if ( empty( $_REQUEST['s'] ) && !$this->has_items() ) 49 | return; 50 | 51 | $input_id = $input_id . '-search-input'; 52 | 53 | if ( ! empty( $_REQUEST['orderby'] ) ) 54 | echo ''; 55 | if ( ! empty( $_REQUEST['order'] ) ) 56 | echo ''; 57 | ?> 58 | 63 | name}_views_field", '' ); 88 | 89 | /** 90 | * Labels for the filed id 91 | * 92 | * @since 1.0.0 93 | * 94 | * @param array $field_labels Field labels in format array( 'field_value' => 'Field Label' ) 95 | * Field values that are not listed here won't get displayed 96 | * 97 | * @return array 98 | */ 99 | $field_labels = apply_filters( "ct_list_{$ct_table->name}_views_field_labels", array() ); 100 | 101 | $views = array(); 102 | 103 | // Check if field ID and labels has been passed and also is field is registered on the table (not as meta) 104 | if( ! empty( $field_id ) && ! empty( $field_labels ) && isset( $ct_table->db->schema->fields[$field_id] ) ) { 105 | 106 | // Get the number of entries per each different field value 107 | $results = $wpdb->get_results( "SELECT {$field_id}, COUNT( * ) AS num_entries FROM {$ct_table->db->table_name} GROUP BY {$field_id}", ARRAY_A ); 108 | $counts = array(); 109 | 110 | // Loop them to build the counts array 111 | foreach( $results as $result ) { 112 | $counts[$result[$field_id]] = absint( $result['num_entries'] ); 113 | } 114 | 115 | $list_link = ct_get_list_link( $ct_table->name ); 116 | $current = isset( $_GET[$field_id] ) ? $_GET[$field_id] : ''; 117 | 118 | // Setup the 'All' view 119 | $all_count = absint( $wpdb->get_var( "SELECT COUNT( * ) FROM {$ct_table->db->table_name}" ) ); 120 | $views['all'] = '' . __( 'All', 'ct' ) . ' (' . $all_count . ')'; 121 | 122 | foreach( $counts as $value => $count ) { 123 | 124 | // Skip fields that are not intended to being displayed 125 | if( ! isset( $field_labels[$value] ) ) { 126 | continue; 127 | } 128 | 129 | $label = $field_labels[$value]; 130 | $url = $list_link . '&' . $field_id . '=' . $value; 131 | 132 | $views[$value] = '' . $label . ' (' . $count . ')' . ''; 133 | } 134 | 135 | } 136 | 137 | /** 138 | * Available filter to past custom views 139 | * 140 | * @since 1.0.0 141 | * 142 | * @param array $views An array of views links. 143 | * Array format: array( 'link_id' => 'link' ) 144 | * Link format: '{label} ({count})' 145 | */ 146 | $views = apply_filters( "{$ct_table->name}_get_views", $views ); 147 | 148 | return $views; 149 | } 150 | 151 | /** 152 | * 153 | * @return array 154 | */ 155 | protected function get_bulk_actions() { 156 | 157 | global $ct_table; 158 | 159 | $actions = array(); 160 | 161 | if ( current_user_can( $ct_table->cap->delete_items ) ) { 162 | $actions['delete'] = __( 'Delete Permanently' ); 163 | } 164 | 165 | $actions = apply_filters( "{$ct_table->name}_bulk_actions", $actions ); 166 | 167 | return $actions; 168 | } 169 | 170 | /** 171 | * 172 | * @return array 173 | */ 174 | protected function get_table_classes() { 175 | global $ct_table; 176 | 177 | return array( 'widefat', 'fixed', 'striped', $ct_table->name ); 178 | } 179 | 180 | /** 181 | * Retrieve the table columns 182 | * 183 | * @access public 184 | * @since 1.0.0 185 | * @return array $columns Array of all the list table columns 186 | */ 187 | public function get_columns() { 188 | global $ct_table; 189 | 190 | $columns = array(); 191 | $bulk_actions = $this->get_bulk_actions(); 192 | 193 | if( ! empty( $bulk_actions ) ) { 194 | $columns['cb'] = ''; 195 | } 196 | 197 | /** 198 | * Filters the columns displayed in the list table of a specific CT table. 199 | * 200 | * @since 1.0.0 201 | * 202 | * @param array $posts_columns An array of column names. 203 | * @param CT_Table $ct_table The table object. 204 | */ 205 | return apply_filters( "manage_{$ct_table->name}_columns", $columns, $ct_table ); 206 | } 207 | 208 | /** 209 | * Retrieve the table's sortable columns 210 | * 211 | * @access public 212 | * @since 1.0.0 213 | * @return array Array of all the sortable columns 214 | */ 215 | public function get_sortable_columns() { 216 | global $ct_table; 217 | 218 | $sortable_columns = array(); 219 | 220 | /** 221 | * Filters the sortable columns in the list table of a specific CT table. 222 | * 223 | * Format: 224 | * 'internal-name' => 'orderby' 225 | * or 226 | * 'internal-name' => array( 'orderby', true ) 227 | * The second format will make the initial sorting order be descending 228 | * 229 | * @since 1.0.0 230 | * 231 | * @param array $sortable_columns An array of column names. 232 | * @param CT_Table $ct_table The table object. 233 | */ 234 | return apply_filters( "manage_{$ct_table->name}_sortable_columns", $sortable_columns, $ct_table ); 235 | } 236 | 237 | /** 238 | * This function renders most of the columns in the list table. 239 | * 240 | * @access public 241 | * @since 1.0.0 242 | * 243 | * @param stdClass $item The current object. 244 | * @param string $column_name The name of the column 245 | * @return string The column value. 246 | */ 247 | public function column_default( $item, $column_name ) { 248 | global $ct_table; 249 | 250 | $value = isset( $item->$column_name ) ? $item->$column_name : ''; 251 | 252 | $primary_key = $ct_table->db->primary_key; 253 | 254 | ob_start(); 255 | /** 256 | * Fires for each custom column of a specific CT table in the list table. 257 | * 258 | * The dynamic portion of the hook name, `$ct_table->name`, refers to the CT table name. 259 | * 260 | * @since 1.0.0 261 | * 262 | * @param string $column_name The name of the column to display. 263 | * @param int $object_id The current object ID. 264 | * @param stdClass $object The current object. 265 | * @param CT_Table $ct_table The CT table object. 266 | */ 267 | do_action( "manage_{$ct_table->name}_custom_column", $column_name, $item->$primary_key, $item, $ct_table ); 268 | $custom_output = ob_get_clean(); 269 | 270 | if( ! empty( $custom_output ) ) { 271 | return $custom_output; 272 | } 273 | 274 | $bulk_actions = $this->get_bulk_actions(); 275 | 276 | $first_column_index = ( ! empty( $bulk_actions ) ) ? 1 : 0; 277 | 278 | $can_edit_item = current_user_can( $ct_table->cap->edit_item, $item->$primary_key ); 279 | $columns = $this->get_columns(); 280 | $columns_keys = array_keys( $columns ); 281 | 282 | if( $column_name === $columns_keys[$first_column_index] && $can_edit_item ) { 283 | 284 | // Turns first column into a text link with url to edit the item 285 | $value = sprintf( '%s', 286 | ct_get_edit_link( $ct_table->name, $item->$primary_key ), 287 | esc_attr( sprintf( __( 'Edit “%s”' ), $value ) ), 288 | $value 289 | ); 290 | 291 | // Small screens toggle 292 | $value .= ''; 293 | 294 | } 295 | 296 | return $value; 297 | } 298 | 299 | /** 300 | * Generates and displays row action links. 301 | * 302 | * @since 4.3.0 303 | * @access protected 304 | * 305 | * @param object $item The item being acted upon. 306 | * @param string $column_name Current column name. 307 | * @param string $primary Primary column name. 308 | * 309 | * @return string Row actions output for posts. 310 | */ 311 | protected function handle_row_actions( $item, $column_name, $primary ) { 312 | if ( $primary !== $column_name ) { 313 | return ''; 314 | } 315 | 316 | global $ct_table; 317 | 318 | $primary_key = $ct_table->db->primary_key; 319 | $actions = array(); 320 | 321 | if ( $ct_table->views->edit && current_user_can( $ct_table->cap->edit_item, $item->$primary_key ) ) { 322 | $actions['edit'] = sprintf( 323 | '%s', 324 | ct_get_edit_link( $ct_table->name, $item->$primary_key ), 325 | esc_attr( __( 'Edit' ) ), 326 | __( 'Edit' ) 327 | ); 328 | } 329 | 330 | if ( current_user_can( $ct_table->cap->delete_item, $item->$primary_key ) ) { 331 | $actions['delete'] = sprintf( 332 | '%s', 333 | ct_get_delete_link( $ct_table->name, $item->$primary_key ), 334 | "return confirm('" . 335 | esc_attr( __( "Are you sure you want to delete this item?\\n\\nClick \\'Cancel\\' to go back, \\'OK\\' to confirm the delete." ) ) . 336 | "');", 337 | esc_attr( __( 'Delete permanently' ) ), 338 | __( 'Delete Permanently' ) 339 | ); 340 | } 341 | 342 | /** 343 | * Filters the array of row action links on the Posts list table. 344 | * 345 | * The filter is evaluated only for non-hierarchical post types. 346 | * 347 | * @since 2.8.0 348 | * 349 | * @param array $actions An array of row action links. Defaults are 350 | * 'Edit', 'Quick Edit', 'Restore, 'Trash', 351 | * 'Delete Permanently', 'Preview', and 'View'. 352 | * @param WP_Post $post The post object. 353 | */ 354 | $actions = apply_filters( "{$ct_table->name}_row_actions", $actions, $item ); 355 | 356 | return $this->row_actions( $actions ); 357 | } 358 | 359 | /** 360 | * Handles the checkbox column output. 361 | * 362 | * @since 1.0.0 363 | * 364 | * @param WP_Post $item The current WP_Post object. 365 | */ 366 | public function column_cb( $item ) { 367 | global $ct_table; 368 | 369 | $primary_key = $ct_table->db->primary_key; 370 | 371 | if ( current_user_can( $ct_table->cap->edit_items ) ): ?> 372 | 375 | 376 |
377 | 378 | $primary_key 383 | ); 384 | ?> 385 |
386 | labels->not_found; 398 | } 399 | 400 | public function prepare_items() { 401 | 402 | global $ct_table, $ct_query; 403 | 404 | // Get per page setting 405 | $per_page = $this->get_items_per_page( 'edit_' . $ct_table->name . '_per_page' ); 406 | 407 | // Update query vars based on settings 408 | $ct_query->query_vars['items_per_page'] = $per_page; 409 | 410 | // Get query results 411 | $this->items = $ct_query->get_results(); 412 | 413 | $total_items = $ct_query->found_results; 414 | 415 | // Setup pagination args based on items found and per page settings 416 | $this->set_pagination_args( array( 417 | 'total_items' => $total_items, 418 | 'per_page' => $per_page, 419 | 'total_pages' => ceil( $total_items / $per_page ) 420 | ) ); 421 | } 422 | 423 | } 424 | 425 | endif; -------------------------------------------------------------------------------- /includes/class-ct-list-view.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 6 | * 7 | * @since 1.0.0 8 | */ 9 | // Exit if accessed directly 10 | defined( 'ABSPATH' ) || exit; 11 | 12 | if ( ! class_exists( 'CT_List_View' ) ) : 13 | 14 | class CT_List_View extends CT_View { 15 | 16 | protected $per_page = 20; 17 | 18 | protected $columns = array(); 19 | 20 | public function __construct( $name, $args ) { 21 | 22 | parent::__construct( $name, $args ); 23 | 24 | $this->per_page = isset( $args['per_page'] ) ? $args['per_page'] : 20; 25 | $this->columns = isset( $args['columns'] ) ? $args['columns'] : array(); 26 | 27 | } 28 | 29 | public function add_hooks() { 30 | 31 | parent::add_hooks(); 32 | 33 | add_filter( "manage_{$this->name}_columns", array( $this, 'get_columns' ) ); 34 | add_filter( "manage_{$this->name}_sortable_columns", array( $this, 'get_sortable_columns' ) ); 35 | 36 | } 37 | 38 | /** 39 | * Set columns passed from view columns arg. 40 | * 41 | * @since 1.0.0 42 | * 43 | * @param array $columns 44 | * 45 | * @return array 46 | */ 47 | public function get_columns( $columns ) { 48 | 49 | foreach( $this->columns as $column => $column_args ) { 50 | 51 | if( is_array( $column_args ) && isset( $column_args['label'] ) ) { 52 | // 'column_name' => array( 'label' => 'Column Label' ) 53 | $columns[$column] = $column_args['label']; 54 | } else if( gettype( $column_args ) === 'string' ) { 55 | // 'column_name' => 'Column Label' 56 | $columns[$column] = $column_args; 57 | } 58 | 59 | } 60 | 61 | return $columns; 62 | 63 | } 64 | 65 | /** 66 | * Set columns passed from view columns arg. 67 | * 68 | * @since 1.0.0 69 | * 70 | * @param array $sortable_columns 71 | * 72 | * @return array 73 | */ 74 | public function get_sortable_columns( $sortable_columns ) { 75 | 76 | foreach( $this->columns as $column => $column_args ) { 77 | 78 | if( is_array( $column_args ) && isset( $column_args['sortable'] ) ) { 79 | // 'column_name' => array( 'sortable' => 'sortable_setup' ) 80 | $sortable_columns[$column] = $column_args['sortable']; 81 | } 82 | 83 | } 84 | 85 | return $sortable_columns; 86 | 87 | } 88 | 89 | public function init() { 90 | 91 | global $ct_registered_tables, $ct_table, $ct_query, $ct_list_table; 92 | 93 | if( ! isset( $ct_registered_tables[$this->name] ) ) { 94 | return; 95 | } 96 | 97 | // Setup CT_Table 98 | $ct_table = $ct_registered_tables[$this->name]; 99 | 100 | // Check for bulk delete 101 | if( isset( $_GET['action'] ) ) { 102 | 103 | if( $_GET['action'] === 'delete' ) { 104 | // Deleting 105 | $this->bulk_delete(); 106 | } 107 | 108 | } 109 | 110 | // Check for delete action 111 | if( isset( $_GET['ct-action'] ) ) { 112 | 113 | if( $_GET['ct-action'] === 'delete' ) { 114 | // Deleting 115 | $this->delete(); 116 | } 117 | 118 | } 119 | 120 | // Setup the query and the list table objects 121 | $ct_query = new CT_Query( $_GET ); 122 | $ct_list_table = new CT_List_Table(); 123 | 124 | } 125 | 126 | /** 127 | * Screen settings text displayed in the Screen Options tab. 128 | * 129 | * @since 1.0.0 130 | * 131 | * @param string $screen_settings Screen settings. 132 | * @param WP_Screen $screen WP_Screen object. 133 | */ 134 | public function screen_settings( $screen_settings, $screen ) { 135 | 136 | $this->render_list_table_columns_preferences(); 137 | $this->render_per_page_options(); 138 | 139 | } 140 | 141 | /** 142 | * Render the list table columns preferences. 143 | * 144 | * @since 1.0.0 145 | */ 146 | public function render_list_table_columns_preferences() { 147 | 148 | global $ct_table, $ct_list_table; 149 | 150 | // Set up vars 151 | $columns = $ct_list_table->get_columns(); 152 | $hidden = get_hidden_columns( $ct_table->name ); 153 | 154 | if ( ! $columns ) 155 | return; 156 | 157 | $legend = ! empty( $columns['_title'] ) ? $columns['_title'] : __( 'Columns' ); 158 | ?> 159 |
160 | 161 | $title ) { 165 | // Can't hide these for they are special 166 | if ( in_array( $column, $special ) || empty( $title ) ) { 167 | continue; 168 | } 169 | 170 | $id = "$column-hide"; 171 | echo '\n"; 174 | } 175 | ?> 176 |
177 | per_page ) { 190 | return; 191 | } 192 | 193 | // Set up vars 194 | $per_page_label = __( 'Number of items per page:' ); 195 | 196 | $option = str_replace( '-', '_', "edit_{$ct_table->name}_per_page" ); 197 | 198 | $per_page = (int) get_user_option( $option ); 199 | 200 | if ( empty( $per_page ) || $per_page < 1 ) 201 | $per_page = $this->per_page; 202 | 203 | $per_page = apply_filters( "{$option}", $per_page ); 204 | 205 | // This needs a submit button 206 | add_filter( 'screen_options_show_submit', '__return_true' ); 207 | 208 | ?> 209 |
210 | 211 | 212 | 213 | 216 | 217 | 218 |
219 | name}_per_page" ) // Per page 247 | ); 248 | 249 | // Columns hidden setting 250 | $columns = $ct_list_table->get_columns(); 251 | $special = array( '_title', 'cb' ); 252 | 253 | foreach ( $columns as $column => $title ) { 254 | // Can't hide these for they are special 255 | if ( in_array( $column, $special ) || empty( $title ) ) 256 | continue; 257 | 258 | $view_settings[] = "$column-hide"; 259 | } 260 | 261 | // If option is on this view settings list, then save it 262 | if( in_array( $option, $view_settings ) ) { 263 | $value_to_set = $value; 264 | } 265 | 266 | return $value_to_set; 267 | 268 | } 269 | 270 | public function bulk_delete() { 271 | 272 | global $ct_table; 273 | 274 | // If not CT object, die 275 | if ( ! $ct_table ) 276 | wp_die( __( 'Invalid item type.' ) ); 277 | 278 | // If not CT object allow ui, die 279 | if ( ! $ct_table->show_ui ) { 280 | wp_die( __( 'Sorry, you are not allowed to delete items of this type.' ) ); 281 | } 282 | 283 | // Nonce check 284 | if ( ! isset( $_REQUEST['_wpnonce'] ) ) { 285 | wp_die( __( 'Sorry, you are not allowed to delete items of this type.' ) ); 286 | } 287 | 288 | if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'bulk-' . sanitize_key( $ct_table->labels->plural_name ) ) ) { 289 | wp_die( __( 'Sorry, you are not allowed to delete items of this type.' ) ); 290 | } 291 | 292 | $object_ids = array(); 293 | 294 | // Check received items 295 | if ( ! empty( $_REQUEST['item'] ) ) { 296 | $object_ids = array_map('intval', $_REQUEST['item']); 297 | } 298 | 299 | $deleted = 0; 300 | 301 | foreach ( (array) $object_ids as $object_id ) { 302 | 303 | // If not current user can delete, die 304 | if ( ! current_user_can( $ct_table->cap->delete_item, $object_id ) ) { 305 | wp_die( __( 'Sorry, you are not allowed to delete this item.' ) ); 306 | } 307 | 308 | if ( ! ct_delete_object( $object_id, true ) ) 309 | wp_die( __( 'Error in deleting.' ) ); 310 | 311 | $deleted++; 312 | } 313 | 314 | $location = add_query_arg( array( 'deleted' => $deleted ), $this->get_link() ); 315 | 316 | wp_redirect( $location ); 317 | exit; 318 | } 319 | 320 | public function delete() { 321 | 322 | global $ct_table; 323 | 324 | // If not CT object, die 325 | if ( ! $ct_table ) { 326 | wp_die( __( 'Invalid item type.' ) ); 327 | } 328 | 329 | // If not CT object allow ui, die 330 | if ( ! $ct_table->show_ui ) { 331 | wp_die( __( 'Sorry, you are not allowed to delete items of this type.' ) ); 332 | } 333 | 334 | $primary_key = $ct_table->db->primary_key; 335 | 336 | // Object ID is required 337 | if( ! isset( $_GET[$primary_key] ) ) { 338 | wp_die( __( 'Sorry, you are not allowed to delete items of this type.' ) ); 339 | } 340 | 341 | $object_id = (int) $_GET[$primary_key]; 342 | 343 | // Nonce check 344 | if ( ! isset( $_REQUEST['_wpnonce'] ) ) { 345 | wp_die( __( 'Sorry, you are not allowed to delete this item.' ) ); 346 | } 347 | 348 | if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'ct_delete_' . $object_id ) ) { 349 | wp_die( __( 'Sorry, you are not allowed to delete this item.' ) ); 350 | } 351 | 352 | // If user can not delete it, bail 353 | if ( ! current_user_can( $ct_table->cap->delete_item, $object_id ) ) { 354 | wp_die( __( 'Sorry, you are not allowed to delete this item.' ) ); 355 | } 356 | 357 | if ( ! ct_delete_object( $object_id ) ) 358 | wp_die( __( 'Error in deleting.' ) ); 359 | 360 | $location = add_query_arg( array( 'deleted' => 1 ), $this->get_link() ); 361 | 362 | wp_redirect( $location ); 363 | exit; 364 | 365 | } 366 | 367 | /** 368 | * View content. 369 | * 370 | * @since 1.0.0 371 | */ 372 | public function render() { 373 | 374 | global $ct_table, $ct_list_table; 375 | 376 | $ct_list_table->prepare_items(); 377 | 378 | $bulk_counts = array( 379 | 'updated' => isset( $_REQUEST['updated'] ) ? absint( $_REQUEST['updated'] ) : 0, 380 | 'locked' => isset( $_REQUEST['locked'] ) ? absint( $_REQUEST['locked'] ) : 0, 381 | 'deleted' => isset( $_REQUEST['deleted'] ) ? absint( $_REQUEST['deleted'] ) : 0, 382 | 'trashed' => isset( $_REQUEST['trashed'] ) ? absint( $_REQUEST['trashed'] ) : 0, 383 | 'untrashed' => isset( $_REQUEST['untrashed'] ) ? absint( $_REQUEST['untrashed'] ) : 0, 384 | ); 385 | 386 | $bulk_messages = array( 387 | 'updated' => _n( '%s item updated.', '%s items updated.', $bulk_counts['updated'] ), 388 | 'locked' => ( 1 == $bulk_counts['locked'] ) ? __( '1 item not updated, somebody is editing it.' ) : 389 | _n( '%s item not updated, somebody is editing it.', '%s items not updated, somebody is editing them.', $bulk_counts['locked'] ), 390 | 'deleted' => _n( '%s item permanently deleted.', '%s items permanently deleted.', $bulk_counts['deleted'] ), 391 | 'trashed' => _n( '%s item moved to the Trash.', '%s items moved to the Trash.', $bulk_counts['trashed'] ), 392 | 'untrashed' => _n( '%s item restored from the Trash.', '%s items restored from the Trash.', $bulk_counts['untrashed'] ), 393 | ); 394 | 395 | /** 396 | * Filters the bulk action updated messages. 397 | * 398 | * @since 1.0.0 399 | * 400 | * @param array $bulk_messages Arrays of messages. Messages are keyed with 'updated', 'locked', 'deleted', 'trashed', and 'untrashed'. 401 | * @param array $bulk_counts Array of item counts for each message, used to build internationalized strings. 402 | */ 403 | $bulk_messages = apply_filters( 'bulk_object_updated_messages', $bulk_messages, $bulk_counts ); 404 | $bulk_counts = array_filter( $bulk_counts ); 405 | 406 | ?> 407 | 408 |
409 | 410 |

labels->plural_name; ?>

411 | 412 | views, 'add' ) && $ct_table->views->add && current_user_can( $ct_table->cap->create_items ) ) : 413 | echo ' ' . esc_html( $ct_table->labels->add_new_item ) . ''; 414 | endif; ?> 415 | 416 |
417 | 418 | $count ) { 422 | if ( isset( $bulk_messages[ $message ] ) ) 423 | $messages[] = sprintf( $bulk_messages[ $message ], number_format_i18n( $count ) ); 424 | 425 | //if ( $message == 'trashed' && isset( $_REQUEST['ids'] ) ) { 426 | //$ids = preg_replace( '/[^0-9,]/', '', $_REQUEST['ids'] ); 427 | //$messages[] = '' . __('Undo') . ''; 428 | //} 429 | } 430 | 431 | if ( $messages ) 432 | echo '

' . join( ' ', $messages ) . '

'; 433 | unset( $messages ); 434 | 435 | $_SERVER['REQUEST_URI'] = remove_query_arg( array( 'locked', 'skipped', 'updated', 'deleted', 'trashed', 'untrashed' ), $_SERVER['REQUEST_URI'] ); 436 | ?> 437 | 438 | views(); ?> 439 | 440 |
441 | 442 | 443 | 444 | search_box( $ct_table->labels->search_items, $ct_table->name ); ?> 445 | 446 | display(); ?> 447 | 448 |
449 | 450 |
451 |
452 | 453 |
454 | 455 | , Ruben Garcia 8 | * 9 | * @since 1.0.0 10 | */ 11 | // Exit if accessed directly 12 | defined( 'ABSPATH' ) || exit; 13 | 14 | /** 15 | * Core class used to manage meta values for posts via the REST API. 16 | * 17 | * @since 1.0.0 18 | * 19 | * @see WP_REST_Meta_Fields 20 | */ 21 | class CT_REST_Meta_Fields extends WP_REST_Meta_Fields { 22 | 23 | /** 24 | * Table name. 25 | * 26 | * @since 1.0.0 27 | * @var string 28 | */ 29 | protected $name; 30 | 31 | /** 32 | * Table Meta table object. 33 | * 34 | * @since 1.0.0 35 | * @access public 36 | * @var CT_Table $table 37 | */ 38 | public $table; 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @since 1.0.0 44 | * 45 | * @param string $name Table name to register fields for. 46 | */ 47 | public function __construct( $name ) { 48 | $this->name = $name; 49 | $this->table = ct_get_table_object( $name ); 50 | 51 | } 52 | 53 | /** 54 | * Retrieves the object meta type. 55 | * 56 | * @since 1.0.0 57 | * 58 | * @return string The meta type. 59 | */ 60 | protected function get_meta_type() { 61 | return $this->table->meta->name; 62 | } 63 | 64 | /** 65 | * Retrieves the object meta subtype. 66 | * 67 | * @since 1.0.0 68 | * 69 | * @return string Subtype for the meta type, or empty string if no specific subtype. 70 | */ 71 | protected function get_meta_subtype() { 72 | return $this->name; 73 | } 74 | 75 | /** 76 | * Retrieves the type for register_rest_field(). 77 | * 78 | * @since 1.0.0 79 | * 80 | * @see register_rest_field() 81 | * 82 | * @return string The REST field type. 83 | */ 84 | public function get_rest_field_type() { 85 | return $this->name; 86 | } 87 | 88 | /** 89 | * Retrieves the meta field value. 90 | * 91 | * @since 1.0.0 92 | * 93 | * @param int $object_id Object ID to fetch meta for. 94 | * @param WP_REST_Request $request Full details about the request. 95 | * @return WP_Error|object Object containing the meta values by name, otherwise WP_Error object. 96 | */ 97 | public function get_value( $object_id, $request ) { 98 | $fields = $this->get_registered_fields(); 99 | $response = array(); 100 | 101 | foreach ( $fields as $meta_key => $args ) { 102 | $name = $args['name']; 103 | $all_values = ct_get_object_meta( $object_id, $meta_key, false ); 104 | if ( $args['single'] ) { 105 | if ( empty( $all_values ) ) { 106 | $value = $args['schema']['default']; 107 | } else { 108 | $value = $all_values[0]; 109 | } 110 | $value = $this->prepare_value_for_response( $value, $request, $args ); 111 | } else { 112 | $value = array(); 113 | foreach ( $all_values as $row ) { 114 | $value[] = $this->prepare_value_for_response( $row, $request, $args ); 115 | } 116 | } 117 | 118 | $response[ $name ] = $value; 119 | } 120 | 121 | return $response; 122 | } 123 | 124 | /** 125 | * Updates a meta value for an object. 126 | * 127 | * @since 4.7.0 128 | * 129 | * @param int $object_id Object ID to update. 130 | * @param string $meta_key Key for the custom field. 131 | * @param string $name Name for the field that is exposed in the REST API. 132 | * @param mixed $value Updated value. 133 | * @return bool|WP_Error True if the meta field was updated, WP_Error otherwise. 134 | */ 135 | protected function update_meta_value( $object_id, $meta_key, $name, $value ) { 136 | $meta_type = $this->get_meta_type(); 137 | if ( ! current_user_can( $this->table->cap->edit_post_meta, $object_id, $meta_key ) ) { 138 | return new WP_Error( 139 | 'rest_cannot_update', 140 | /* translators: %s: custom field key */ 141 | sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ), 142 | array( 'key' => $name, 'status' => rest_authorization_required_code() ) 143 | ); 144 | } 145 | 146 | // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false. 147 | $old_value = ct_get_object_meta( $object_id, $meta_key ); 148 | $subtype = get_object_subtype( $meta_type, $object_id ); 149 | 150 | if ( 1 === count( $old_value ) ) { 151 | if ( (string) sanitize_meta( $meta_key, $value, $meta_type, $subtype ) === $old_value[0] ) { 152 | return true; 153 | } 154 | } 155 | 156 | if ( ! ct_update_object_meta( $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) { 157 | return new WP_Error( 158 | 'rest_meta_database_error', 159 | __( 'Could not update meta value in database.' ), 160 | array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR ) 161 | ); 162 | } 163 | 164 | return true; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /includes/class-ct-table-meta.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 6 | * 7 | * @since 1.0.0 8 | */ 9 | // Exit if accessed directly 10 | defined( 'ABSPATH' ) || exit; 11 | 12 | if ( ! class_exists( 'CT_Table_Meta' ) ) : 13 | 14 | class CT_Table_Meta { 15 | 16 | /** 17 | * Table key. 18 | * 19 | * @since 1.0.0 20 | * @access public 21 | * @var string $name 22 | */ 23 | public $name; 24 | 25 | /** 26 | * Table Meta database. 27 | * 28 | * @since 1.0.0 29 | * @access public 30 | * @var CT_DataBase $db 31 | */ 32 | public $db; 33 | 34 | /** 35 | * Table Meta table object. 36 | * 37 | * @since 1.0.0 38 | * @access public 39 | * @var CT_Table $table 40 | */ 41 | public $table; 42 | 43 | /** 44 | * CT_Table_Meta constructor. 45 | * @param CT_Table $table 46 | */ 47 | public function __construct( $table ) { 48 | 49 | $this->table = $table; 50 | $this->name = $this->table->name . '_meta'; 51 | 52 | $this->db = new CT_DataBase( $this->name, array( 53 | 'version' => 1, 54 | 'global' => $this->table->db->global, 55 | 'schema' => array( 56 | 'meta_id' => array( 57 | 'type' => 'bigint', 58 | 'length' => 20, 59 | 'unsigned' => true, 60 | 'nullable' => false, 61 | 'auto_increment' => true, 62 | 'primary_key' => true 63 | ), 64 | $this->table->db->primary_key => array( 65 | 'type' => 'bigint', 66 | 'length' => 20, 67 | 'unsigned' => true, 68 | 'nullable' => false, 69 | 'default' => 0, 70 | 'key' => true 71 | ), 72 | 'meta_key' => array( 73 | 'type' => 'varchar', 74 | 'length' => 255, 75 | 'nullable' => true, 76 | 'default' => null, 77 | 'key' => true 78 | ), 79 | 'meta_value' => array( 80 | 'type' => 'longtext', 81 | 'nullable' => true, 82 | 'default' => null 83 | ), 84 | ) 85 | ) ); 86 | 87 | } 88 | 89 | } 90 | 91 | endif; -------------------------------------------------------------------------------- /includes/class-ct-table.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 8 | * 9 | * @since 1.0.0 10 | */ 11 | // Exit if accessed directly 12 | defined( 'ABSPATH' ) || exit; 13 | 14 | if ( ! class_exists( 'CT_Table' ) ) : 15 | 16 | class CT_Table { 17 | 18 | /** 19 | * Table key. 20 | * 21 | * @since 1.0.0 22 | * @access public 23 | * @var string $name 24 | */ 25 | public $name; 26 | 27 | /** 28 | * Table database. 29 | * 30 | * @since 1.0.0 31 | * @access public 32 | * @var CT_DataBase $db 33 | */ 34 | public $db; 35 | 36 | /** 37 | * Table ciews. 38 | * 39 | * @since 1.0.0 40 | * @access public 41 | * @var stdClass $views 42 | */ 43 | public $views; 44 | 45 | /** 46 | * Table Meta (if supports contains 'meta'). 47 | * 48 | * @since 1.0.0 49 | * @access public 50 | * @var CT_Table_Meta $meta 51 | */ 52 | public $meta; 53 | 54 | /** 55 | * Table key. 56 | * 57 | * @since 1.0.0 58 | * @access public 59 | * @var string $name 60 | */ 61 | public $singular; 62 | 63 | /** 64 | * Table key. 65 | * 66 | * @since 1.0.0 67 | * @access public 68 | * @var string $name 69 | */ 70 | public $plural; 71 | 72 | /** 73 | * Name of the post type shown in the menu. Usually plural. 74 | * 75 | * @since 1.0.0 76 | * @access public 77 | * @var string $label 78 | */ 79 | public $label; 80 | 81 | /** 82 | * Labels object for this post type. 83 | * 84 | * If not set, post labels are inherited for non-hierarchical types 85 | * and page labels for hierarchical ones. 86 | * 87 | * @see get_post_type_labels() 88 | * 89 | * @since 1.0.0 90 | * @access public 91 | * @var object $labels 92 | */ 93 | public $labels; 94 | 95 | /** 96 | * Whether to exclude posts with this post type from front end search 97 | * results. 98 | * 99 | * Default is the opposite value of $public. 100 | * 101 | * @since 1.0.0 102 | * @access public 103 | * @var bool $exclude_from_search 104 | */ 105 | public $exclude_from_search = null; 106 | 107 | /** 108 | * Whether queries can be performed on the front end for the post type as part of `parse_request()`. 109 | * 110 | * Endpoints would include: 111 | * - `?post_type={post_type_key}` 112 | * - `?{post_type_key}={single_post_slug}` 113 | * - `?{post_type_query_var}={single_post_slug}` 114 | * 115 | * Default is the value of $public. 116 | * 117 | * @since 1.0.0 118 | * @access public 119 | * @var bool $publicly_queryable 120 | */ 121 | public $publicly_queryable = null; 122 | 123 | /** 124 | * Whether to generate and allow a UI for managing this post type in the admin. 125 | * 126 | * Default is the value of $public. 127 | * 128 | * @since 1.0.0 129 | * @access public 130 | * @var bool $show_ui 131 | */ 132 | public $show_ui = null; 133 | 134 | /** 135 | * Where to show the post type in the admin menu. 136 | * 137 | * To work, $show_ui must be true. If true, the post type is shown in its own top level menu. If false, no menu is 138 | * shown. If a string of an existing top level menu (eg. 'tools.php' or 'edit.php?post_type=page'), the post type 139 | * will be placed as a sub-menu of that. 140 | * 141 | * Default is the value of $show_ui. 142 | * 143 | * @since 1.0.0 144 | * @access public 145 | * @var bool $show_in_menu 146 | */ 147 | public $show_in_menu = null; 148 | 149 | /** 150 | * Makes this post type available for selection in navigation menus. 151 | * 152 | * Default is the value $public. 153 | * 154 | * @since 1.0.0 155 | * @access public 156 | * @var bool $show_in_nav_menus 157 | */ 158 | public $show_in_nav_menus = null; 159 | 160 | /** 161 | * Makes this post type available via the admin bar. 162 | * 163 | * Default is the value of $show_in_menu. 164 | * 165 | * @since 1.0.0 166 | * @access public 167 | * @var bool $show_in_admin_bar 168 | */ 169 | public $show_in_admin_bar = null; 170 | 171 | /** 172 | * The position in the menu order the post type should appear. 173 | * 174 | * To work, $show_in_menu must be true. Default null (at the bottom). 175 | * 176 | * @since 1.0.0 177 | * @access public 178 | * @var int $menu_position 179 | */ 180 | public $menu_position = null; 181 | 182 | /** 183 | * The URL to the icon to be used for this menu. 184 | * 185 | * Pass a base64-encoded SVG using a data URI, which will be colored to match the color scheme. 186 | * This should begin with 'data:image/svg+xml;base64,'. Pass the name of a Dashicons helper class 187 | * to use a font icon, e.g. 'dashicons-chart-pie'. Pass 'none' to leave div.wp-menu-image empty 188 | * so an icon can be added via CSS. 189 | * 190 | * Defaults to use the posts icon. 191 | * 192 | * @since 1.0.0 193 | * @access public 194 | * @var string $menu_icon 195 | */ 196 | public $menu_icon = null; 197 | 198 | /** 199 | * The string to use to build the read, edit, and delete capabilities. 200 | * 201 | * May be passed as an array to allow for alternative plurals when using 202 | * this argument as a base to construct the capabilities, e.g. 203 | * array( 'story', 'stories' ). Default 'post'. 204 | * 205 | * @since 1.0.0 206 | * @access public 207 | * @var string $capability_type 208 | */ 209 | public $capability_type = 'post'; 210 | 211 | /** 212 | * Whether to use the internal default meta capability handling. 213 | * 214 | * Default false. 215 | * 216 | * @since 1.0.0 217 | * @access public 218 | * @var bool $map_meta_cap 219 | */ 220 | public $map_meta_cap = false; 221 | 222 | /** 223 | * Provide a callback function that sets up the meta boxes for the edit form. 224 | * 225 | * Do `remove_meta_box()` and `add_meta_box()` calls in the callback. Default null. 226 | * 227 | * @since 1.0.0 228 | * @access public 229 | * @var string $register_meta_box_cb 230 | */ 231 | public $register_meta_box_cb = null; 232 | 233 | /** 234 | * An array of taxonomy identifiers that will be registered for the post type. 235 | * 236 | * Taxonomies can be registered later with `register_taxonomy()` or `register_taxonomy_for_object_type()`. 237 | * 238 | * Default empty array. 239 | * 240 | * @since 1.0.0 241 | * @access public 242 | * @var array $taxonomies 243 | */ 244 | public $taxonomies = array(); 245 | 246 | /** 247 | * Whether there should be post type archives, or if a string, the archive slug to use. 248 | * 249 | * Will generate the proper rewrite rules if $rewrite is enabled. Default false. 250 | * 251 | * @since 1.0.0 252 | * @access public 253 | * @var bool|string $has_archive 254 | */ 255 | public $has_archive = false; 256 | 257 | /** 258 | * Sets the query_var key for this post type. 259 | * 260 | * Defaults to $post_type key. If false, a post type cannot be loaded at `?{query_var}={post_slug}`. 261 | * If specified as a string, the query `?{query_var_string}={post_slug}` will be valid. 262 | * 263 | * @since 1.0.0 264 | * @access public 265 | * @var string|bool $query_var 266 | */ 267 | public $query_var; 268 | 269 | /** 270 | * Whether to allow this post type to be exported. 271 | * 272 | * Default true. 273 | * 274 | * @since 1.0.0 275 | * @access public 276 | * @var bool $can_export 277 | */ 278 | public $can_export = true; 279 | 280 | /** 281 | * Whether to delete posts of this type when deleting a user. 282 | * 283 | * If true, posts of this type belonging to the user will be moved to trash when then user is deleted. 284 | * If false, posts of this type belonging to the user will *not* be trashed or deleted. 285 | * If not set (the default), posts are trashed if post_type_supports( 'author' ). 286 | * Otherwise posts are not trashed or deleted. Default null. 287 | * 288 | * @since 1.0.0 289 | * @access public 290 | * @var bool $delete_with_user 291 | */ 292 | public $delete_with_user = null; 293 | 294 | /** 295 | * Whether this table is a native or "built-in" post_type. 296 | * 297 | * Default false. 298 | * 299 | * @since 1.0.0 300 | * @access public 301 | * @var bool $_builtin 302 | */ 303 | public $_builtin = false; 304 | 305 | /** 306 | * URL segment to use for edit link of this table. 307 | * 308 | * Default 'post.php?post=%d'. 309 | * 310 | * @since 1.0.0 311 | * @access public 312 | * @var string $_edit_link 313 | */ 314 | public $_edit_link = 'post.php?post=%d'; 315 | 316 | /** 317 | * Table capabilities. 318 | * 319 | * @since 1.0.0 320 | * @access public 321 | * @var object $cap 322 | */ 323 | public $cap; 324 | 325 | /** 326 | * Table capability to access. 327 | * 328 | * @since 1.0.0 329 | * @access public 330 | * @var string $capability 331 | */ 332 | public $capability; 333 | 334 | /** 335 | * Triggers the handling of rewrites for this post type. 336 | * 337 | * Defaults to true, using $post_type as slug. 338 | * 339 | * @since 1.0.0 340 | * @access public 341 | * @var array|false $rewrite 342 | */ 343 | public $rewrite; 344 | 345 | /** 346 | * The features supported by the post type. 347 | * 348 | * @since 1.0.0 349 | * @access public 350 | * @var array|bool $supports 351 | */ 352 | public $supports; 353 | 354 | /** 355 | * Whether this post type should appear in the REST API. 356 | * 357 | * Default false. If true, standard endpoints will be registered with 358 | * respect to $rest_base and $rest_controller_class. 359 | * 360 | * @since 4.7.4 361 | * @access public 362 | * @var bool $show_in_rest 363 | */ 364 | public $show_in_rest; 365 | 366 | /** 367 | * The base path for this post type's REST API endpoints. 368 | * 369 | * @since 4.7.4 370 | * @access public 371 | * @var string|bool $rest_base 372 | */ 373 | public $rest_base; 374 | 375 | /** 376 | * The controller for this post type's REST API endpoints. 377 | * 378 | * Custom controllers must extend WP_REST_Controller. 379 | * 380 | * @since 4.7.4 381 | * @access public 382 | * @var string|bool $rest_controller_class 383 | */ 384 | public $rest_controller_class; 385 | 386 | /** 387 | * Constructor. 388 | * 389 | * Will populate object properties from the provided arguments and assign other 390 | * default properties based on that information. 391 | * 392 | * @since 1.0.0 393 | * @access public 394 | * 395 | * @see ct_register_table() 396 | * 397 | * @param string $name Table key. 398 | * @param array|string $args Optional. Array or string of arguments for registering a post type. 399 | * Default empty array. 400 | */ 401 | public function __construct( $name, $args = array() ) { 402 | 403 | // Table name 404 | $this->name = $name; 405 | 406 | $this->set_props( $args ); 407 | 408 | } 409 | 410 | /** 411 | * Sets table properties. 412 | * 413 | * @since 1.0.0 414 | * @access public 415 | * 416 | * @param array|string $args Array or string of arguments for registering a post type. 417 | */ 418 | public function set_props( $args ) { 419 | $args = wp_parse_args( $args ); 420 | 421 | /** 422 | * Filters the arguments for registering a table. 423 | * 424 | * @since 1.0.0 425 | * 426 | * @param array $args Array of arguments for registering a post type. 427 | * @param string $post_type Table key. 428 | */ 429 | $args = apply_filters( 'ct_register_table_args', $args, $this->name ); 430 | 431 | $has_edit_link = ! empty( $args['_edit_link'] ); 432 | 433 | // Args prefixed with an underscore are reserved for internal use. 434 | $defaults = array( 435 | 'singular' => $this->name, 436 | 'plural' => $this->name . 's', 437 | 'labels' => array(), 438 | 'description' => '', 439 | 'group' => '', 440 | 'public' => false, 441 | 'hierarchical' => false, 442 | 'exclude_from_search' => null, 443 | 'publicly_queryable' => null, 444 | 'show_ui' => null, 445 | 'show_in_menu' => null, 446 | 'show_in_nav_menus' => null, 447 | 'show_in_admin_bar' => null, 448 | 'menu_position' => null, 449 | 'menu_icon' => null, 450 | 'capability_type' => 'item', 451 | 'capabilities' => array(), 452 | 'map_meta_cap' => null, 453 | 'supports' => array(), 454 | 'register_meta_box_cb' => null, 455 | //'taxonomies' => array(), 456 | 'has_archive' => false, 457 | 'rewrite' => true, 458 | 'query_var' => true, 459 | 'can_export' => true, 460 | 'delete_with_user' => null, 461 | //'_builtin' => false, 462 | //'_edit_link' => 'post.php?post=%d', 463 | // Rest defaults 464 | 'show_in_rest' => false, 465 | 'rest_base' => false, 466 | 'rest_controller_class' => false, 467 | // Database defaults 468 | 'primary_key' => '', 469 | 'version' => 1, 470 | 'global' => false, 471 | //'schema' => '', 472 | 'engine' => 'InnoDB', 473 | // Shortcuts 474 | 'capability' => '', 475 | ); 476 | 477 | $args = array_merge( $defaults, $args ); 478 | 479 | $args['name'] = $this->name; 480 | 481 | if ( empty( $args['group'] ) ) { 482 | $args['group'] = strtok( $this->name, '_'); 483 | } 484 | 485 | // If not set, default to the setting for public. 486 | if ( null === $args['publicly_queryable'] ) { 487 | $args['publicly_queryable'] = $args['public']; 488 | } 489 | 490 | // If not set, default to the setting for public. 491 | if ( null === $args['show_ui'] ) { 492 | $args['show_ui'] = $args['public']; 493 | } 494 | 495 | // If not set, default to the setting for show_ui. 496 | if ( null === $args['show_in_menu'] || ! $args['show_ui'] ) { 497 | $args['show_in_menu'] = $args['show_ui']; 498 | } 499 | 500 | // If not set, default to the whether the full UI is shown. 501 | if ( null === $args['show_in_admin_bar'] ) { 502 | $args['show_in_admin_bar'] = (bool) $args['show_in_menu']; 503 | } 504 | 505 | // If not set, default to the setting for public. 506 | if ( null === $args['show_in_nav_menus'] ) { 507 | $args['show_in_nav_menus'] = $args['public']; 508 | } 509 | 510 | // If not set, default to true if not public, false if public. 511 | if ( null === $args['exclude_from_search'] ) { 512 | $args['exclude_from_search'] = ! $args['public']; 513 | } 514 | 515 | // Back compat with quirky handling in version 3.0. #14122. 516 | if ( empty( $args['capabilities'] ) && null === $args['map_meta_cap'] && in_array( $args['capability_type'], array( 'post', 'page' ) ) ) { 517 | $args['map_meta_cap'] = true; 518 | } 519 | 520 | // If not set, default to false. 521 | if ( null === $args['map_meta_cap'] ) { 522 | $args['map_meta_cap'] = false; 523 | } 524 | 525 | // If there's no specified edit link and no UI, remove the edit link. 526 | if ( ! $args['show_ui'] && ! $has_edit_link ) { 527 | $args['_edit_link'] = ''; 528 | } 529 | 530 | $this->cap = ct_get_table_capabilities( (object) $args ); 531 | $this->capability = $args['capability']; 532 | 533 | unset( $args['capabilities'] ); 534 | 535 | if ( is_array( $args['capability_type'] ) ) { 536 | $args['capability_type'] = $args['capability_type'][0]; 537 | } 538 | 539 | if ( false !== $args['query_var'] ) { 540 | if ( true === $args['query_var'] ) { 541 | $args['query_var'] = $this->name; 542 | } else { 543 | $args['query_var'] = sanitize_title_with_dashes( $args['query_var'] ); 544 | } 545 | } 546 | 547 | if ( false !== $args['rewrite'] && ( is_admin() || '' != get_option( 'permalink_structure' ) ) ) { 548 | if ( ! is_array( $args['rewrite'] ) ) { 549 | $args['rewrite'] = array(); 550 | } 551 | if ( empty( $args['rewrite']['slug'] ) ) { 552 | $args['rewrite']['slug'] = $this->name; 553 | } 554 | if ( ! isset( $args['rewrite']['with_front'] ) ) { 555 | $args['rewrite']['with_front'] = true; 556 | } 557 | if ( ! isset( $args['rewrite']['pages'] ) ) { 558 | $args['rewrite']['pages'] = true; 559 | } 560 | if ( ! isset( $args['rewrite']['feeds'] ) || ! $args['has_archive'] ) { 561 | $args['rewrite']['feeds'] = (bool) $args['has_archive']; 562 | } 563 | if ( ! isset( $args['rewrite']['ep_mask'] ) ) { 564 | if ( isset( $args['permalink_epmask'] ) ) { 565 | $args['rewrite']['ep_mask'] = $args['permalink_epmask']; 566 | } else { 567 | $args['rewrite']['ep_mask'] = EP_PERMALINK; 568 | } 569 | } 570 | } 571 | 572 | foreach ( $args as $property_name => $property_value ) { 573 | $this->$property_name = $property_value; 574 | } 575 | 576 | $this->singular = $args['singular']; 577 | $this->plural = $args['plural']; 578 | 579 | $labels = (array) ct_get_table_labels( $this ); 580 | 581 | // Custom defined labels overrides default 582 | if( isset( $args['labels'] ) && is_array( $args['labels'] ) ) { 583 | $labels = wp_parse_args( $args['labels'], $labels ); 584 | } 585 | 586 | $this->labels = (object) $labels; 587 | 588 | $this->label = $this->labels->name; 589 | 590 | // Table database 591 | 592 | if( isset( $args['db'] ) ) { 593 | if( is_array( $args['db'] ) ) { 594 | // Table as array of args to pass to CT_DataBase 595 | $this->db = new CT_DataBase( $this->name, $args['db'] ); 596 | } else if( $args['db'] instanceof CT_DataBase || is_subclass_of( $args['db'], 'CT_DataBase' ) ) { 597 | // Table as custom object 598 | $this->db = $args['db']; 599 | } 600 | } else { 601 | // Default database initialization 602 | $this->db = new CT_DataBase( $this->name, $args ); 603 | } 604 | 605 | // Views (list, add, edit) 606 | 607 | $views_defaults = array( 608 | 'list' => array( 609 | 'page_title' => $this->labels->plural_name, 610 | 'menu_title' => $this->labels->all_items, 611 | 'menu_slug' => $this->name, 612 | 'parent_slug' => $this->name, 613 | 'show_in_menu' => $this->show_ui, 614 | 615 | // Specific view args 616 | 'per_page' => 20, 617 | 'columns' => array(), 618 | ), 619 | 'add' => array( 620 | 'page_title' => $this->labels->add_new, 621 | 'menu_title' => $this->labels->add_new, 622 | 'menu_slug' => 'add_' . $this->name, 623 | 'parent_slug' => $this->name, 624 | 'show_in_menu' => $this->show_ui, 625 | 626 | // Specific view args 627 | 'columns' => 2, 628 | ), 629 | 'edit' => array( 630 | 'page_title' => $this->labels->edit_item, 631 | 'menu_title' => $this->labels->edit_item, 632 | 'menu_slug' => 'edit_' . $this->name, 633 | 'parent_slug' => '', 634 | 'show_in_menu' => false, 635 | 636 | // Specific view args 637 | 'columns' => 2, 638 | ), 639 | ); 640 | 641 | if( isset( $args['views'] ) && is_array( $args['views'] ) ) { 642 | 643 | $views = array(); 644 | 645 | // Ensure default views (list, add, edit) are in 646 | foreach( $views_defaults as $view => $view_args ) { 647 | if( ! isset( $args['views'][$view] ) ) { 648 | $args['views'][$view] = $view_args; 649 | } 650 | } 651 | 652 | foreach( $args['views'] as $view => $view_args ) { 653 | 654 | if( is_array( $view_args ) ) { 655 | 656 | // Parse default view args 657 | if( isset( $views_defaults[$view] ) ) { 658 | $view_args = wp_parse_args( $view_args, $views_defaults[$view] ); 659 | } 660 | 661 | // View as array of args to pass to CT_View 662 | switch( $view ) { 663 | case 'list': 664 | $views[$view] = new CT_List_View( $this->name, $view_args ); 665 | break; 666 | case 'add': 667 | $views[$view] = new CT_Edit_View( $this->name, $view_args ); 668 | break; 669 | case 'edit': 670 | $views[$view] = new CT_Edit_View( $this->name, $view_args ); 671 | break; 672 | default: 673 | $views[$view] = new CT_View( $this->name, $view_args ); 674 | break; 675 | } 676 | } else if( $view_args instanceof CT_View || is_subclass_of( $view_args, 'CT_View' ) ) { 677 | // View as custom object 678 | $views[$view] = $view_args; 679 | } 680 | } 681 | 682 | // Ensure to add all default views 683 | foreach( array( 'list', 'add', 'edit' ) as $view ) { 684 | if( ! isset( $views[$view] ) ) { 685 | $views[$view] = false; 686 | } 687 | } 688 | 689 | $this->views = (object) $views; 690 | 691 | } else { 692 | // Default views initialization 693 | $this->views = (object) array( 694 | 'list' => new CT_List_View( $this->name, $views_defaults['list'] ), 695 | 'add' => new CT_Edit_View( $this->name, $views_defaults['add'] ), 696 | 'edit' => new CT_Edit_View( $this->name, $views_defaults['edit'] ), 697 | ); 698 | } 699 | 700 | // Meta data 701 | if( in_array( 'meta', $this->supports ) ) { 702 | $this->meta = new CT_Table_Meta( $this ); 703 | } 704 | } 705 | 706 | } 707 | 708 | endif; -------------------------------------------------------------------------------- /includes/class-ct-view.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 6 | * 7 | * @since 1.0.0 8 | */ 9 | // Exit if accessed directly 10 | defined( 'ABSPATH' ) || exit; 11 | 12 | if ( ! class_exists( 'CT_View' ) ) : 13 | 14 | class CT_View { 15 | 16 | /** 17 | * @var string View name 18 | */ 19 | protected $name = ''; 20 | 21 | /** 22 | * @var array View args 23 | */ 24 | protected $args = array(); 25 | 26 | /** 27 | * CT_View constructor. 28 | * 29 | * @since 1.0.0 30 | * 31 | * @param string $name 32 | * @param array $args 33 | */ 34 | public function __construct( $name, $args ) { 35 | 36 | $this->name = $name; 37 | 38 | $this->args = wp_parse_args( $args, array( 39 | 'menu_title' => ucfirst( $this->name ), 40 | 'page_title' => ucfirst( $this->name ), 41 | 'menu_slug' => $this->name, 42 | 'parent_slug' => '', 43 | 'show_in_menu' => true, 44 | 'menu_icon' => '', 45 | 'menu_position' => null, 46 | 'capability' => 'manage_options', 47 | ) ); 48 | 49 | $this->add_hooks(); 50 | 51 | } 52 | 53 | /** 54 | * View hooks (called on constructor). 55 | * 56 | * @since 1.0.0 57 | */ 58 | public function add_hooks() { 59 | 60 | add_action( 'admin_init', array( $this, 'admin_init' ) ); 61 | 62 | // Note: sub-menus need to be registered after parent 63 | add_action( 'admin_menu', array( $this, 'admin_menu' ), empty( $this->args['parent_slug'] ) ? 10 : 11 ); 64 | 65 | add_filter( 'parent_file', array( $this, 'parent_file' ), 10 ); 66 | 67 | add_filter( 'submenu_file', array( $this, 'submenu_file' ), 10, 2 ); 68 | 69 | add_action( 'adminmenu', array( $this, 'restore_plugin_page' ), 10, 2 ); 70 | 71 | add_filter( 'screen_options_show_screen', array( $this, 'show_screen_options' ), 10, 2 ); 72 | 73 | add_filter( 'screen_settings', array( $this, 'maybe_screen_settings' ), 10, 2 ); 74 | 75 | add_filter( 'admin_init', array( $this, 'maybe_set_screen_settings' ), 11 ); 76 | add_filter( 'ct-set-screen-option', array( $this, 'set_screen_settings' ), 10, 3 ); 77 | 78 | } 79 | 80 | public function show_screen_options( $show_screen, $screen ) { 81 | 82 | if( ! $this->is_current_view() ) { 83 | return $show_screen; 84 | } 85 | 86 | $screen_slug = explode( '_page_', $screen->id ); 87 | 88 | if( isset( $screen_slug[1] ) && $screen_slug[1] === $this->args['menu_slug'] ) { 89 | return true; 90 | } 91 | 92 | return $show_screen; 93 | 94 | } 95 | 96 | /** 97 | * Check if current screen is own. 98 | * 99 | * @since 1.0.0 100 | * 101 | * @param string $screen_settings Screen settings. 102 | * @param WP_Screen $screen WP_Screen object. 103 | * 104 | * @return string $screen_settings 105 | */ 106 | public function maybe_screen_settings( $screen_settings, $screen ) { 107 | 108 | if( ! $this->is_current_view() ) { 109 | return $screen_settings; 110 | } 111 | 112 | $screen_slug = explode( '_page_', $screen->id ); 113 | 114 | // Check if current screen matches this menu slug 115 | if( isset( $screen_slug[1] ) && $screen_slug[1] === $this->args['menu_slug'] ) { 116 | 117 | global $ct_registered_tables, $ct_table; 118 | 119 | if( ! isset( $ct_registered_tables[$this->name] ) ) { 120 | return $screen_settings; 121 | } 122 | 123 | // Set up global vars 124 | $ct_table = $ct_registered_tables[$this->name]; 125 | 126 | ob_start(); 127 | $this->screen_settings( $screen_settings, $screen ); 128 | $screen_settings .= ob_get_clean(); 129 | 130 | } 131 | 132 | return $screen_settings; 133 | 134 | } 135 | 136 | /** 137 | * Screen settings text displayed in the Screen Options tab. 138 | * 139 | * @since 1.0.0 140 | * 141 | * @param string $screen_settings Screen settings. 142 | * @param WP_Screen $screen WP_Screen object. 143 | */ 144 | public function screen_settings( $screen_settings, $screen ) { 145 | // Override 146 | } 147 | 148 | /** 149 | * Saves view options. 150 | * 151 | * Function based on set_screen_options() 152 | * 153 | * @since 1.0.0 154 | * 155 | * @see set_screen_options() 156 | */ 157 | function maybe_set_screen_settings() { 158 | 159 | if( ! $this->is_current_view() ) { 160 | return; 161 | } 162 | 163 | if ( isset( $_POST['wp_screen_options'] ) && is_array( $_POST['wp_screen_options'] ) ) { 164 | check_admin_referer( 'screen-options-nonce', 'screenoptionnonce' ); 165 | 166 | if ( ! $user = wp_get_current_user() ) { 167 | return; 168 | } 169 | 170 | $option = $_POST['wp_screen_options']['option']; 171 | $value = $_POST['wp_screen_options']['value']; 172 | 173 | if ( $option != sanitize_key( $option ) ) { 174 | return; 175 | } 176 | 177 | $option = str_replace('-', '_', $option); 178 | 179 | /** 180 | * Filters a screen option value before it is set. 181 | * 182 | * The filter can also be used to modify non-standard [items]_per_page 183 | * settings. See the parent function for a full list of standard options. 184 | * 185 | * Returning false to the filter will skip saving the current option. 186 | * 187 | * @since 1.0.0 188 | * 189 | * @see set_screen_options() 190 | * 191 | * @param bool|int $value Screen option value. Default false to skip. 192 | * @param string $option The option name. 193 | * @param int $value The number of rows to use. 194 | */ 195 | $value = apply_filters( 'ct-set-screen-option', false, $option, $value ); 196 | 197 | if ( false === $value ) 198 | return; 199 | 200 | update_user_meta( $user->ID, $option, $value ); 201 | 202 | $url = remove_query_arg( array( 'pagenum', 'apage', 'paged' ), wp_get_referer() ); 203 | if ( isset( $_POST['mode'] ) ) { 204 | $url = add_query_arg( array( 'mode' => $_POST['mode'] ), $url ); 205 | } 206 | 207 | wp_safe_redirect( $url ); 208 | exit; 209 | } 210 | } 211 | 212 | /** 213 | * Screen option value before it is set. 214 | * 215 | * The filter can also be used to modify non-standard [items]_per_page 216 | * settings. See the parent function for a full list of standard options. 217 | * 218 | * Returning false to the filter will skip saving the current option. 219 | * 220 | * @since 1.0.0 221 | * 222 | * @see set_screen_options() 223 | * 224 | * @param bool|int $value_to_set Screen option value to set. Default false to skip. 225 | * @param string $option The option name. 226 | * @param int $value The option value. 227 | * 228 | * @return bool|mixed False to skip or any other value to set as option value 229 | */ 230 | public function set_screen_settings( $value_to_set, $option, $value ) { 231 | 232 | // Override 233 | 234 | return $value_to_set; 235 | 236 | } 237 | 238 | /** 239 | * Create a new menu 240 | * 241 | * @since 1.0.0 242 | */ 243 | public function admin_menu() { 244 | 245 | // Override capability by the table capability 246 | $ct_table = ct_get_table_object( $this->name ); 247 | 248 | if( ! empty( $ct_table->capability ) ) { 249 | $this->args['capability'] = $ct_table->capability; 250 | } 251 | 252 | if( ! $this->args['show_in_menu'] ) { 253 | 254 | add_submenu_page( '', $this->args['page_title'], $this->args['menu_title'], $this->args['capability'], $this->args['menu_slug'], array( $this, 'render' ) ); 255 | 256 | } else { 257 | 258 | if( empty( $this->args['parent_slug'] ) ) { 259 | // View menu 260 | add_menu_page( $this->args['page_title'], $this->args['menu_title'], $this->args['capability'], $this->args['menu_slug'], array( $this, 'render' ), $this->args['menu_icon'], $this->args['menu_position'] ); 261 | } else { 262 | // View sub menu 263 | add_submenu_page( $this->args['parent_slug'], $this->args['page_title'], $this->args['menu_title'], $this->args['capability'], $this->args['menu_slug'], array( $this, 'render' ) ); 264 | } 265 | 266 | } 267 | 268 | } 269 | 270 | /** 271 | * Parent file fix when a view is registered in a submenu 272 | * 273 | * @since 1.0.0 274 | */ 275 | public function parent_file( $parent_file ) { 276 | 277 | global $ct_table, $plugin_page; 278 | 279 | if( ! $this->is_current_view() ) { 280 | return $parent_file; 281 | } 282 | 283 | $list_view_args = $ct_table->views->list->args; 284 | 285 | // If not empty parent slug, override actual parent slug 286 | if( ! empty( $this->args['parent_slug'] ) ) { 287 | $parent_file = $this->args['parent_slug']; 288 | 289 | if( $this->args['menu_slug'] !== $list_view_args['menu_slug'] ) { 290 | // Hack required to make parent file work because get overwritten on get_admin_page_parent() function 291 | $plugin_page = null; 292 | } 293 | } 294 | 295 | // If we are on an add or edit view and list is displayed on menu, apply the list parent slug 296 | if( $this->args['menu_slug'] !== $list_view_args['menu_slug'] ) { 297 | 298 | if( $list_view_args['show_in_menu'] && ! empty( $list_view_args['parent_slug'] ) ) { 299 | $parent_file = $list_view_args['parent_slug']; 300 | 301 | // Hack required to make parent file work because get overwritten on get_admin_page_parent() function 302 | $plugin_page = null; 303 | } 304 | 305 | } 306 | 307 | return $parent_file; 308 | 309 | } 310 | 311 | /** 312 | * Submenu file fix when a view is registered in a submenu 313 | * 314 | * @since 1.0.0 315 | */ 316 | public function submenu_file( $submenu_file, $parent_file ) { 317 | 318 | global $ct_table; 319 | 320 | if( ! $this->is_current_view() ) { 321 | return $submenu_file; 322 | } 323 | 324 | $list_view_args = $ct_table->views->list->args; 325 | 326 | // If we are on an add or edit view and list is displayed on menu, then highlight list view 327 | if( $this->args['menu_slug'] !== $list_view_args['menu_slug'] ) { 328 | if( $list_view_args['show_in_menu'] && ! empty( $list_view_args['parent_slug'] ) ) { 329 | $submenu_file = $list_view_args['menu_slug']; 330 | } 331 | } 332 | 333 | return $submenu_file; 334 | 335 | } 336 | 337 | /** 338 | * Restore global $plugin_page since it was required to get overwritten on parent_file() 339 | * 340 | * @since 1.0.0 341 | */ 342 | public function restore_plugin_page() { 343 | 344 | global $ct_table, $plugin_page; 345 | 346 | if( ! $this->is_current_view() ) { 347 | return; 348 | } 349 | 350 | $list_view_args = $ct_table->views->list->args; 351 | 352 | // If we are on an add or edit view and list is displayed on menu, restore plugin page too 353 | if( $this->args['menu_slug'] !== $list_view_args['menu_slug'] ) { 354 | 355 | // If not empty parent slug then restore plugin page 356 | if( ! empty( $this->args['parent_slug'] ) ) { 357 | $plugin_page = $this->args['menu_slug']; 358 | } 359 | 360 | if( $list_view_args['show_in_menu'] && ! empty( $list_view_args['parent_slug'] ) ) { 361 | $plugin_page = $this->args['menu_slug']; 362 | } 363 | } 364 | 365 | } 366 | 367 | public function is_current_view() { 368 | 369 | global $ct_registered_tables, $pagenow; 370 | 371 | if( $pagenow !== 'admin.php' ) { 372 | return false; 373 | } 374 | 375 | if( ! isset( $_GET['page'] ) ) { 376 | return false; 377 | } 378 | 379 | if( empty( $_GET['page'] ) || $_GET['page'] !== $this->args['menu_slug'] ) { 380 | return false; 381 | } 382 | 383 | if( ! isset( $ct_registered_tables[$this->name] ) ) { 384 | return false; 385 | } 386 | 387 | return true; 388 | } 389 | 390 | public function get_slug() { 391 | return $this->args['menu_slug']; 392 | } 393 | 394 | public function get_link() { 395 | return admin_url( "admin.php?page=" . $this->args['menu_slug'] ); 396 | } 397 | 398 | /** 399 | * View admin init. 400 | * 401 | * This function is called on admin_init hook. 402 | * Includes some checks to determine if the init() function should be called. 403 | * 404 | * @since 1.0.0 405 | */ 406 | public function admin_init() { 407 | 408 | global $ct_registered_tables, $ct_table; 409 | 410 | if( ! $this->is_current_view() ) { 411 | return; 412 | } 413 | 414 | // Setup the global CT_Table object for this screen 415 | $ct_table = $ct_registered_tables[$this->name]; 416 | 417 | // Run the init function 418 | $this->init(); 419 | 420 | } 421 | 422 | /** 423 | * View init. 424 | * 425 | * Run redirects here to avoid "headers already sent" error. 426 | * 427 | * @since 1.0.0 428 | */ 429 | public function init() { 430 | 431 | do_action( "ct_init_{$this->name}_view", $this ); 432 | 433 | } 434 | 435 | /** 436 | * View content. 437 | * 438 | * @since 1.0.0 439 | */ 440 | public function render() { 441 | 442 | do_action( "ct_render_{$this->name}_view", $this ); 443 | 444 | } 445 | 446 | } 447 | 448 | endif; -------------------------------------------------------------------------------- /includes/class-ct.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 9 | * @copyright Copyright (c) GamiPress 10 | */ 11 | // Exit if accessed directly 12 | defined( 'ABSPATH' ) || exit; 13 | 14 | /* 15 | * Copyright (c) GamiPress (contact@gamipress.com), Ruben Garcia (rubengcdev@gmail.com) 16 | * 17 | * This program is free software: you can redistribute it and/or modify it 18 | * under the terms of the GNU Affero General Public License, version 3, 19 | * as published by the Free Software Foundation. 20 | * 21 | * This program is distributed in the hope that it will be useful, but 22 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 23 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 24 | * Public License for more details. 25 | * 26 | * You should have received a copy of the GNU Affero General Public License 27 | * along with this program. If not, see . 28 | */ 29 | 30 | if ( ! class_exists( 'CT' ) ) : 31 | 32 | final class CT { 33 | 34 | /** 35 | * @var CT $instance The one true CT 36 | * @since 1.0.0 37 | */ 38 | private static $instance; 39 | 40 | /** 41 | * Get active instance 42 | * 43 | * @access public 44 | * @since 1.0.0 45 | * @return object self::$instance The one true CT 46 | */ 47 | public static function instance() { 48 | 49 | if( ! self::$instance ) { 50 | 51 | self::$instance = new CT(); 52 | self::$instance->includes(); 53 | self::$instance->compatibility(); 54 | self::$instance->hooks(); 55 | self::$instance->load_textdomain(); 56 | 57 | } 58 | 59 | return self::$instance; 60 | 61 | } 62 | 63 | /** 64 | * Include CT files 65 | * 66 | * @access private 67 | * @since 1.0.0 68 | * @return void 69 | */ 70 | private function includes() { 71 | 72 | // WP_List_Table dependencies 73 | if( ! function_exists( 'convert_to_screen' ) ) { 74 | require_once ABSPATH . 'wp-admin/includes/template.php'; 75 | } 76 | 77 | if( ! function_exists( 'get_column_headers' ) ) { 78 | require_once ABSPATH . 'wp-admin/includes/screen.php'; 79 | } 80 | 81 | // Includes required WP_List_Table class 82 | if( ! class_exists( 'WP_List_Table' ) ) { 83 | require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; 84 | } 85 | 86 | // CT_Table and CT_Table_Meta classes 87 | require_once CT_DIR . 'includes/class-ct-table.php'; 88 | require_once CT_DIR . 'includes/class-ct-table-meta.php'; 89 | // Database and schema related classes 90 | require_once CT_DIR . 'includes/class-ct-database.php'; 91 | require_once CT_DIR . 'includes/class-ct-database-schema.php'; 92 | require_once CT_DIR . 'includes/class-ct-database-schema-updater.php'; 93 | // Rest API 94 | require_once CT_DIR . 'includes/class-ct-rest-controller.php'; 95 | require_once CT_DIR . 'includes/class-ct-rest-meta-fields.php'; 96 | // CT_Query and CT_List_Table classes 97 | require_once CT_DIR . 'includes/class-ct-query.php'; 98 | require_once CT_DIR . 'includes/class-ct-list-table.php'; 99 | // Views (List and edit) 100 | require_once CT_DIR . 'includes/class-ct-view.php'; 101 | require_once CT_DIR . 'includes/class-ct-list-view.php'; 102 | require_once CT_DIR . 'includes/class-ct-edit-view.php'; 103 | // Rest of includes 104 | require_once CT_DIR . 'includes/functions.php'; 105 | require_once CT_DIR . 'includes/hooks.php'; 106 | 107 | } 108 | 109 | /** 110 | * Include CT compatibility files 111 | * 112 | * @access private 113 | * @since 1.0.0 114 | * @return void 115 | */ 116 | private function compatibility() { 117 | 118 | require_once CT_DIR . 'compatibility/cmb2.php'; 119 | 120 | } 121 | 122 | /** 123 | * Setup CT hooks 124 | * 125 | * @access private 126 | * @since 1.0.0 127 | * @return void 128 | */ 129 | private function hooks() { 130 | 131 | add_action( 'plugins_loaded', array( $this, 'init' ), 11 ); 132 | add_action( 'after_setup_theme', array( $this, 'init' ), 11 ); 133 | 134 | } 135 | 136 | public function init() { 137 | 138 | if ( did_action( 'ct_init' ) ) { 139 | return; 140 | } 141 | 142 | // Setup role caps for CT capabilities 143 | ct_populate_roles(); 144 | 145 | // Trigger CT init hook 146 | do_action( 'ct_init' ); 147 | 148 | if( is_admin() ) { 149 | 150 | // Trigger CT admin init hook 151 | do_action( 'ct_admin_init' ); 152 | 153 | } 154 | 155 | } 156 | 157 | /** 158 | * Internationalization 159 | * 160 | * @access public 161 | * @since 1.0.0 162 | * @return void 163 | */ 164 | public function load_textdomain() { 165 | // Set filter for language directory 166 | $lang_dir = CT_DIR . '/languages/'; 167 | $lang_dir = apply_filters( 'ct_languages_directory', $lang_dir ); 168 | 169 | // Traditional WordPress plugin locale filter 170 | $locale = apply_filters( 'plugin_locale', get_locale(), 'ct' ); 171 | $mofile = sprintf( '%1$s-%2$s.mo', 'ct', $locale ); 172 | 173 | // Setup paths to current locale file 174 | $mofile_local = $lang_dir . $mofile; 175 | $mofile_global = WP_LANG_DIR . '/ct/' . $mofile; 176 | 177 | if( file_exists( $mofile_global ) ) { 178 | // Look in global /wp-content/languages/ct/ folder 179 | load_textdomain( 'ct', $mofile_global ); 180 | } elseif( file_exists( $mofile_local ) ) { 181 | // Look in local /wp-content/plugins/ct/languages/ folder 182 | load_textdomain( 'ct', $mofile_local ); 183 | } else { 184 | // Load the default language files 185 | load_plugin_textdomain( 'ct', false, $lang_dir ); 186 | } 187 | } 188 | 189 | } 190 | 191 | 192 | /** 193 | * The main function responsible for returning the one true CT instance to functions everywhere 194 | * 195 | * @since 1.0.0 196 | * @return \CT The one true CT 197 | */ 198 | function ct() { 199 | return CT::instance(); 200 | } 201 | 202 | ct(); 203 | 204 | endif; -------------------------------------------------------------------------------- /includes/hooks.php: -------------------------------------------------------------------------------- 1 | show_in_rest ) { 23 | continue; 24 | } 25 | 26 | $class = ! empty( $ct_table->rest_controller_class ) ? $ct_table->rest_controller_class : 'CT_REST_Controller'; 27 | 28 | // Skip if rest controller class doesn't exists 29 | if ( ! class_exists( $class ) ) { 30 | continue; 31 | } 32 | 33 | $controller = new $class( $ct_table->name ); 34 | 35 | // Check if controller is subclass of WP_REST_Controller to check if should call to the register_routes() function 36 | if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) { 37 | continue; 38 | } 39 | 40 | $controller->register_routes(); 41 | 42 | } 43 | 44 | // Trigger CT rest API init hook 45 | do_action( 'ct_rest_api_init' ); 46 | } 47 | add_action( 'rest_api_init', 'ct_rest_api_init', 9 ); -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | , Ruben Garcia 10 | * @copyright GamiPress , Ruben Garcia 11 | * @credits Justin Sternberg (https://jtsternberg.com), Jhon James Jacob (https://jjj.blog) 12 | * @license GPL-2.0+ 13 | * @version 1.0.7 14 | * @link https://gamipress.com 15 | */ 16 | 17 | /* 18 | * Copyright (c) GamiPress (contact@gamipress.com), Ruben Garcia (rubengcdev@gmail.com) 19 | * 20 | * This program is free software; you can redistribute it and/or modify 21 | * it under the terms of the GNU General Public License, version 2 or, at 22 | * your discretion, any later version, as published by the Free 23 | * Software Foundation. 24 | * 25 | * This program is distributed in the hope that it will be useful, 26 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 28 | * GNU General Public License for more details. 29 | * 30 | * You should have received a copy of the GNU General Public License 31 | * along with this program; if not, write to the Free Software 32 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 33 | */ 34 | 35 | /** 36 | * Loader versioning: http://jtsternberg.github.io/wp-lib-loader/ 37 | */ 38 | if ( ! class_exists( 'CT_Loader_107', false ) ) { 39 | 40 | class CT_Loader_107 { 41 | 42 | /** 43 | * CT_Loader version number 44 | * @var string 45 | * @since 1.0.0 46 | */ 47 | const VERSION = '1.0.7'; 48 | 49 | /** 50 | * Setup constants 51 | * 52 | * @since 1.0.0 53 | */ 54 | private function constants() { 55 | 56 | // Version 57 | define( 'CT_VER', self::VERSION ); 58 | 59 | // File 60 | define( 'CT_FILE', __FILE__ ); 61 | 62 | // Path 63 | define( 'CT_DIR', plugin_dir_path( __FILE__ ) ); 64 | 65 | // URL 66 | define( 'CT_URL', plugin_dir_url( __FILE__ ) ); 67 | 68 | // Debug 69 | define( 'CT_DEBUG', false ); 70 | 71 | } 72 | 73 | /** 74 | * Starts the version checking process. 75 | * Creates CT_LOADED definition for early detection by other scripts. 76 | * 77 | * Hooks CT_Loader inclusion to the ct_loader_load hook 78 | * on a high priority which decrements (increasing the priority) with 79 | * each version release. 80 | * 81 | * @since 1.0.0 82 | */ 83 | public function __construct() { 84 | 85 | if ( ! defined( 'CT_LOADER_PRIORITY' ) ) { 86 | // Calculate priority converting version into a number (eg: 1.0.0 to 100) 87 | define( 'CT_LOADER_PRIORITY', 99999 - absint( str_replace( '.', '', self::VERSION ) ) ); 88 | } 89 | 90 | if ( ! defined( 'CT_LOADED' ) ) { 91 | // A constant you can use to check if Custom Tables (CT) is loaded for your plugins/themes with CT dependency. 92 | // Can also be used to determine the priority of the hook in use for the currently loaded version. 93 | define( 'CT_LOADED', CT_LOADER_PRIORITY ); 94 | } 95 | 96 | // Use the hook system to ensure only the newest version is loaded. 97 | add_action( 'ct_loader_load', array( $this, 'include_lib' ), CT_LOADER_PRIORITY ); 98 | 99 | // Try to fire our hook as soon as possible,including right now (required for activation hooks). 100 | self::fire_hook(); 101 | 102 | // Hook in to the first hook we have available and fire our `ct_loader_load' hook. 103 | add_action( 'muplugins_loaded', array( __CLASS__, 'fire_hook' ), 9 ); 104 | add_action( 'plugins_loaded', array( __CLASS__, 'fire_hook' ), 9 ); 105 | add_action( 'after_setup_theme', array( __CLASS__, 'fire_hook' ), 9 ); 106 | } 107 | 108 | /** 109 | * Fires the ct_loader_load action hook. 110 | * 111 | * @since 1.0.0 112 | */ 113 | public static function fire_hook() { 114 | if ( ! did_action( 'ct_loader_load' ) ) { 115 | // Then fire our hook. 116 | do_action( 'ct_loader_load' ); 117 | } 118 | } 119 | 120 | /** 121 | * A final check if CT_Loader exists before kicking off 122 | * our CT_Loader loading. 123 | * 124 | * @since 1.0.0 125 | */ 126 | public function include_lib() { 127 | if ( class_exists( 'CT', false ) ) { 128 | return; 129 | } 130 | 131 | $this->constants(); 132 | 133 | // Include and initiate Custom Tables (CT) class. 134 | require_once CT_DIR . 'includes/class-ct.php'; 135 | } 136 | 137 | } 138 | 139 | // Kick it off. 140 | new CT_Loader_107; 141 | } 142 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # About Custom Tables # 2 | 3 | Custom Tables (CT) is a WordPress developers toolkit to handle custom database table workflow similar to WordPress CPT. 4 | 5 | Check [example.php](./example.php) file included on this project to see a few examples to get it working. 6 | 7 | CT has been developed as internal library for [GamiPress](https://gamipress.com) plugin in order to bring to GamiPress's logs and user earnings tables the same features as WordPress post types (admin UI, cached query, rest API endpoints, etc). 8 | 9 | Important: CT public API is in development phase, this means current version is unstable and much of the current features will change. To use this library on live project be sure you know you are doing! 10 | 11 | Contributions are really appreciated! Looking for help to standarize functions and hooks as well as for documentation. 12 | 13 | ## Features (work in progress) ## 14 | 15 | Custom table registration: 16 | 17 | - [x] Custom table registration (like registering a WordPress post type) 18 | - [x] Automatic table creation if not exists 19 | - [x] Easy field definition 20 | - [x] Schema parser 21 | - [x] Automatic schema updater (yay!) 22 | - [x] Database parameters (collate, engine, etc) 23 | - [x] Ability to show or hide from admin UI (disable UI for a desired table) 24 | - [x] Custom Capabilities (with support for administrators) 25 | - [x] Meta data functionality 26 | - [x] Query class to handled cached queries (like WP_Query but for custom tables) 27 | - [x] Rest API support (custom table and meta data) 28 | 29 | List view (with features similar to WP tables): 30 | 31 | - [x] Pagination 32 | - [x] Search 33 | - [x] Sortable Columns 34 | - [x] Bulk actions 35 | - [x] User screen settings 36 | - [x] List view views 37 | - [ ] Trash functionality? 38 | - [ ] Revisions functionality? 39 | - [x] Delete Permanently action 40 | 41 | Edit View (similar to WP edit screen): 42 | 43 | - [x] Meta boxes 44 | - [x] Screen options 45 | - [x] Show hide Meta boxes 46 | - [x] Allow user to toggle view columns 47 | - [x] Allow define edit view columns (to force to 1 column) 48 | - [x] Delete Permanently action 49 | 50 | Other features 51 | 52 | - [x] CMB2 support 53 | - [ ] Documentation (help wanted!) 54 | - [x] Add WP Lib Loader to always load the newest version (http://jtsternberg.github.io/wp-lib-loader/) 55 | 56 | ## Plugins ## 57 | 58 | - [Ajax List Table](https://github.com/rubengc/ct-ajax-list-table): Utility to render a Custom Tables (CT) List Table with ajax searching and pagination. 59 | - [Rest API Docs](https://github.com/rubengc/ct-rest-api-docs): Rest API docs generator for Custom Tables (CT). 60 | 61 | ## Changelog ## 62 | 63 | **1.0.7** 64 | 65 | * **Bug Fixes** 66 | * Fixed PHP notices caused by add_submenu_page() function when passing null as first parameter. 67 | 68 | **1.0.6** 69 | 70 | **Improvements** 71 | * Added more nonce checks to prevent CSRF attacks. 72 | 73 | **1.0.5** 74 | 75 | **Improvements** 76 | * Added more nonce checks to prevent CSRF attacks. 77 | 78 | **1.0.4** 79 | 80 | **Improvements** 81 | * Reduced the number of "Show tables" queries. 82 | 83 | **1.0.3** 84 | 85 | **Improvements** 86 | - Added support for CMB2 fields data removal if field has "multiple" set to "true". 87 | 88 | **1.0.2** 89 | 90 | **Bug Fixes** 91 | - Make use of the min() function when defining length of the table keys (thanks to @mholubowski). 92 | 93 | **1.0.1** 94 | 95 | **Improvements** 96 | - Prevent to add index length for DATETIME fields (thanks to @mholubowski, fixes #9). 97 | - Quote all fields and indexes during database creation. 98 | 99 | **1.0.0** 100 | Initial release. --------------------------------------------------------------------------------