_register_post_type(); // We are on WP "init" hook already. } /** * Register the Custom Post Type which the tables use. * * @since 1.0.0 */ protected function _register_post_type(): void { /** * Filters the "Custom Post Type" that TablePress uses for storing tables in the database. * * @since 1.0.0 * * @param string $post_type The "Custom Post Type" that TablePress uses. */ $this->post_type = apply_filters( 'tablepress_post_type', $this->post_type ); $post_type_args = array( 'labels' => array( 'name' => 'TablePress Tables', ), 'public' => false, 'show_ui' => false, 'query_var' => false, 'rewrite' => false, 'capability_type' => 'tablepress_table', // This ensures, that WP's regular CPT UI respects our capabilities. 'map_meta_cap' => false, // Integrated WP mapping does not fit our needs, therefore use our own in a filter. 'supports' => array( 'title', 'editor', 'excerpt', 'revisions' ), 'can_export' => true, ); /** * Filters the arguments for the registration of the "Custom Post Type" that TablePress uses. * * @since 1.0.0 * * @param array $post_type_args Arguments for the registration of the TablePress "Custom Post Type". */ $post_type_args = apply_filters( 'tablepress_post_type_args', $post_type_args ); register_post_type( $this->post_type, $post_type_args ); } /** * Insert a post with the correct Custom Post Type and default values in the the wp_posts table in the database. * * @since 1.0.0 * * @param array $post Post to insert. * @return int|WP_Error Post ID of the inserted post on success, WP_Error on error. */ public function insert( array $post ) /* : int|WP_Error */ { $default_post = array( 'ID' => false, // false on new insert, but existing post ID on update. 'comment_status' => 'closed', 'ping_status' => 'closed', 'post_category' => false, 'post_content' => '', 'post_excerpt' => '', 'post_parent' => 0, 'post_password' => '', 'post_status' => 'publish', 'post_title' => '', 'post_type' => $this->post_type, 'tags_input' => '', 'to_ping' => '', ); $post = array_merge( $default_post, $post ); // WP expects everything to be slashed. $post = wp_slash( $post ); // Remove balanceTags() from sanitize_post(), as it can destroy the JSON when messing with HTML. remove_filter( 'content_save_pre', 'balanceTags', 50 ); remove_filter( 'excerpt_save_pre', 'balanceTags', 50 ); /* * Remove possible KSES filtering, as it can destroy the JSON when messing with HTML. * KSES filtering is done to table cells individually, when saving. */ $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); if ( $has_kses ) { kses_remove_filters(); } // Remove filter that adds `rel="noopener" to HTML tags, but destroys JSON code. See https://core.trac.wordpress.org/ticket/46316. $has_targeted_link_rel_filters = ( false !== has_filter( 'content_save_pre', 'wp_targeted_link_rel' ) ); if ( $has_targeted_link_rel_filters ) { wp_remove_targeted_link_rel_filters(); } $post_id = wp_insert_post( $post, true ); // Restore removed content filters. add_filter( 'content_save_pre', 'balanceTags', 50 ); add_filter( 'excerpt_save_pre', 'balanceTags', 50 ); if ( $has_kses ) { kses_init_filters(); } if ( $has_targeted_link_rel_filters ) { wp_init_targeted_link_rel_filters(); } // In rare cases, `wp_insert_post()` returns 0 as the post ID, when an error happens, so it's converted to a WP_Error here. if ( 0 === $post_id ) { // @phpstan-ignore-line (False-positive in the PHPStan WordPress stubs.) return new WP_Error( 'post_insert', '' ); } return $post_id; } /** * Update an existing post with the correct Custom Post Type and default values in the the wp_posts table in the database. * * @since 1.0.0 * * @param array $post Post. * @return int|WP_Error Post ID of the updated post on success, WP_Error on error. */ public function update( array $post ) /* : int|WP_Error */ { $default_post = array( 'ID' => false, // false on new insert, but existing post ID on update. 'comment_status' => 'closed', 'ping_status' => 'closed', 'post_category' => false, 'post_content' => '', 'post_excerpt' => '', 'post_parent' => 0, 'post_password' => '', 'post_status' => 'publish', 'post_title' => '', 'post_type' => $this->post_type, 'tags_input' => '', 'to_ping' => '', ); $post = array_merge( $default_post, $post ); // WP expects everything to be slashed. $post = wp_slash( $post ); // Remove balanceTags() from sanitize_post(), as it can destroy the JSON when messing with HTML. remove_filter( 'content_save_pre', 'balanceTags', 50 ); remove_filter( 'excerpt_save_pre', 'balanceTags', 50 ); /* * Remove possible KSES filtering, as it can destroy the JSON when messing with HTML. * KSES filtering is done to table cells individually, when saving. */ $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); if ( $has_kses ) { kses_remove_filters(); } // Remove filter that adds `rel="noopener" to HTML tags, but destroys JSON code. See https://core.trac.wordpress.org/ticket/46316. $has_targeted_link_rel_filters = ( false !== has_filter( 'content_save_pre', 'wp_targeted_link_rel' ) ); if ( $has_targeted_link_rel_filters ) { wp_remove_targeted_link_rel_filters(); } $post_id = wp_update_post( $post, true ); // Restore removed content filters. add_filter( 'content_save_pre', 'balanceTags', 50 ); add_filter( 'excerpt_save_pre', 'balanceTags', 50 ); if ( $has_kses ) { kses_init_filters(); } if ( $has_targeted_link_rel_filters ) { wp_init_targeted_link_rel_filters(); } return $post_id; } /** * Get a post from the wp_posts table in the database. * * @since 1.0.0 * * @param int $post_id Post ID. * @return WP_Post|false Post on success, false on error. */ public function get( int $post_id ) /* : WP_Post|false */ { $post = get_post( $post_id ); if ( is_null( $post ) ) { return false; } return $post; } /** * Delete a post (and all revisions) from the wp_posts table in the database. * * @since 1.0.0 * * @param int $post_id Post ID. * @return WP_Post|false|null Post data on success, false or null on failure. */ public function delete( int $post_id ) /* : WP_Post|false|null */ { return wp_delete_post( $post_id, true ); // true means force delete, although for CPTs this is automatic in this function. } /** * Move a post to the trash (if trash is globally enabled), instead of directly deleting the post. * (yet unused) * * @since 1.0.0 * * @param int $post_id Post ID. * @return WP_Post|false|null Post data on success, false or null on failure. */ public function trash( int $post_id ) /* : WP_Post|false|null */ { return wp_trash_post( $post_id ); } /** * Restore a post from the trash. * (yet unused) * * @since 1.0.0 * * @param int $post_id Post ID. * @return WP_Post|false|null Post on success, false or null on error. */ public function untrash( int $post_id ) /* : WP_Post|false */ { return wp_untrash_post( $post_id ); } /** * Load all posts with one query, to prime the cache. * * @since 1.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * @see get_post() * * @param int[] $all_post_ids List of Post IDs. * @param bool $update_meta_cache Optional. Whether to update the Post Meta Cache (for table options and visibility). */ public function load_posts( array $all_post_ids, bool $update_meta_cache = true ): void { global $wpdb; // Split post loading, to save memory. $offset = 0; $length = 100; // 100 posts at a time $number_of_posts = count( $all_post_ids ); while ( $offset < $number_of_posts ) { $post_ids = array_slice( $all_post_ids, $offset, $length ); // Don't load posts that are in the cache already. $post_ids = _get_non_cached_ids( $post_ids, 'posts' ); if ( ! empty( $post_ids ) ) { $post_ids_list = implode( ',', $post_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $posts = $wpdb->get_results( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID IN ({$post_ids_list})" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared update_post_cache( $posts ); if ( $update_meta_cache ) { // Get all post meta data for all table posts, @see get_post_meta(). update_meta_cache( 'post', $post_ids ); } } $offset += $length; // next array_slice() $offset. } } /** * Count the number of posts with the model's CPT in the wp_posts table in the database. * (currently for debug only) * * @since 1.0.0 * * @return int Number of posts. */ public function count_posts(): int { return array_sum( (array) wp_count_posts( $this->post_type ) ); // Original return value is object with the counts for each post_status. } /** * Add a post meta field to a post. * * @since 1.0.0 * * @param int $post_id ID of the post for which the field shall be added. * @param string $field Name of the post meta field. * @param string $value Value of the post meta field (not slashed). * @return bool True on success, false on error. */ public function add_meta_field( int $post_id, string $field, string $value ): bool { // WP expects a slashed value. $value = wp_slash( $value ); $success = add_post_meta( $post_id, $field, $value, true ); // true means unique. // Make sure that $success is a boolean, as add_post_meta() returns an ID or false. $success = ( false === $success ) ? false : true; return $success; } /** * Update the value of a post meta field of a post. * * If the field does not yet exist, it is added. * * @since 1.0.0 * * @param int $post_id ID of the post for which the field shall be updated. * @param string $field Name of the post meta field. * @param string $value Value of the post meta field (not slashed). * @return bool True on success, false on error. */ public function update_meta_field( int $post_id, string $field, string $value ): bool { $prev_value = (string) get_post_meta( $post_id, $field, true ); // No need to update, if values are equal (also, update_post_meta() would return false for this). if ( $prev_value === $value ) { return true; } // WP expects a slashed value. $value = wp_slash( $value ); return (bool) update_post_meta( $post_id, $field, $value, $prev_value ); } /** * Get the value of a post meta field of a post. * * @since 1.0.0 * * @param int $post_id ID of the post for which the field shall be retrieved. * @param string $field Name of the post meta field. * @return string Value of the meta field. */ public function get_meta_field( int $post_id, string $field ): string { return get_post_meta( $post_id, $field, true ); // true means single value. } /** * Delete a post meta field of a post. * (yet unused) * * @since 1.0.0 * * @param int $post_id ID of the post of which the field shall be deleted. * @param string $field Name of the post meta field. * @return bool True on success, false on error. */ public function delete_meta_field( int $post_id, string $field ): bool { return delete_post_meta( $post_id, $field, true ); // true means single value. } /** * Return the Custom Post Type that TablePress uses. * * @since 1.5.0 * * @return string The used Custom Post Type. */ public function get_post_type(): string { return $this->post_type; } } // class TablePress_Post_Model