prefix . 'threepo_' . $name; } public static function install( bool $network_wide = false ): void { if ( $network_wide ) { wp_die( esc_html__( 'Activate Post Order separately on each site.', '3task-post-order' ) ); } global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset = $wpdb->get_charset_collate(); $table = self::table( 'contexts' ); dbDelta( "CREATE TABLE $table ( id bigint unsigned NOT NULL AUTO_INCREMENT, context_key varchar(191) NOT NULL, kind varchar(8) NOT NULL, object_type varchar(32) NOT NULL, taxonomy varchar(32) NOT NULL DEFAULT '', term_id bigint unsigned NOT NULL DEFAULT 0, parent_id bigint unsigned NOT NULL DEFAULT 0, label varchar(120) NOT NULL, scope varchar(12) NOT NULL DEFAULT 'backend', revision bigint unsigned NOT NULL DEFAULT 0, import_id bigint unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY context_key (context_key) ) ENGINE=InnoDB $charset;" ); $table = self::table( 'entries' ); dbDelta( "CREATE TABLE $table ( context_id bigint unsigned NOT NULL, object_id bigint unsigned NOT NULL, rank_value bigint NOT NULL, PRIMARY KEY (context_id,object_id), KEY ordered (context_id,rank_value,object_id) ) ENGINE=InnoDB $charset;" ); $table = self::table( 'history' ); dbDelta( "CREATE TABLE $table ( id bigint unsigned NOT NULL AUTO_INCREMENT, context_id bigint unsigned NOT NULL, operation varchar(36) NOT NULL, fingerprint varchar(64) NOT NULL, actor bigint unsigned NOT NULL, revision bigint unsigned NOT NULL, action varchar(12) NOT NULL, object_id bigint unsigned NOT NULL DEFAULT 0, previous_id bigint unsigned NOT NULL DEFAULT 0, next_id bigint unsigned NOT NULL DEFAULT 0, created datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY operation (context_id,operation), KEY recent (context_id,revision) ) ENGINE=InnoDB $charset;" ); $table = self::table( 'imports' ); dbDelta( "CREATE TABLE $table ( id bigint unsigned NOT NULL AUTO_INCREMENT, status varchar(24) NOT NULL, source_hash varchar(64) NOT NULL, plan longtext NOT NULL, cursor_index bigint unsigned NOT NULL DEFAULT 0, cursor_offset bigint unsigned NOT NULL DEFAULT 0, was_active tinyint NOT NULL DEFAULT 0, created datetime NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB $charset;" ); $table = self::table( 'snapshots' ); dbDelta( "CREATE TABLE $table ( context_id bigint unsigned NOT NULL, revision bigint unsigned NOT NULL, object_id bigint unsigned NOT NULL, rank_value bigint NOT NULL, PRIMARY KEY (context_id,revision,object_id) ) ENGINE=InnoDB $charset;" ); update_option( 'threepo_schema', 5, true ); } public static function error( string $code, string $message, int $status = 400 ): WP_Error { return new WP_Error( 'threepo_' . $code, $message, array( 'status' => $status ) ); } /** * @return array|null */ public static function get( int $id ): ?array { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id=%d', self::table( 'contexts' ), $id ), ARRAY_A ); return $row ? self::cast( $row ) : null; } /** * @param array $row Validated values. * @return array */ public static function cast( array $row ): array { foreach ( array( 'id', 'term_id', 'parent_id', 'revision', 'import_id' ) as $key ) { $row[ $key ] = (int) $row[ $key ]; } return $row; } /** * @param array $context Validated values. */ public static function allowed( array $context ): bool { if ( 'term' === $context['kind'] ) { $tax = get_taxonomy( $context['object_type'] ); return $tax && current_user_can( $tax->cap->manage_terms ); } $type = get_post_type_object( $context['object_type'] ); return $type && current_user_can( $type->cap->edit_others_posts ); } /** * @param array $data Validated values. * @return array|\WP_Error */ public static function create( array $data ) { global $wpdb; if ( ! current_user_can( 'manage_options' ) ) { return self::error( 'forbidden', __( 'You cannot create an ordering context.', '3task-post-order' ), 403 ); } if ( defined( 'ICL_SITEPRESS_VERSION' ) || function_exists( 'pll_languages_list' ) ) { return self::error( 'language', __( 'Multilingual ordering is not supported in this version.', '3task-post-order' ) ); } if ( ! self::allowed( $data ) ) { return self::error( 'forbidden', __( 'You cannot manage this content type.', '3task-post-order' ), 403 ); } $kind = $data['kind']; $type = $data['object_type']; $tax = $data['taxonomy']; $term = $data['term_id']; $parent = $data['parent_id']; $object = 'post' === $kind ? get_post_type_object( $type ) : get_taxonomy( $type ); if ( ! $object || ! $object->show_ui || ( 'post' === $kind && 'attachment' === $type ) ) { return self::error( 'type', __( 'Choose a supported content type.', '3task-post-order' ) ); } if ( $tax && ( 'post' !== $kind || ! is_object_in_taxonomy( $type, $tax ) || ! $term || ! term_exists( $term, $tax ) ) ) { return self::error( 'taxonomy', __( 'The category does not belong to this content type.', '3task-post-order' ) ); } if ( ! $tax && $term ) { return self::error( 'taxonomy', __( 'Choose the taxonomy for this term.', '3task-post-order' ) ); } if ( $parent ) { $p = 'post' === $kind ? get_post( $parent ) : get_term( $parent, $type ); if ( ! $p || is_wp_error( $p ) || ! $object->hierarchical || ( 'post' === $kind && ( $p->post_type !== $type || ! current_user_can( 'edit_post', $parent ) ) ) ) { return self::error( 'parent', __( 'The parent is not available in this context.', '3task-post-order' ) ); } } $key = implode( ':', array( $kind, $type, $tax, $term, $parent ) ); $old = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE context_key=%s', self::table( 'contexts' ), $key ) ); if ( $old ) { return self::get( (int) $old ); } $row = array( 'context_key' => $key, 'kind' => $kind, 'object_type' => $type, 'taxonomy' => $tax, 'term_id' => $term, 'parent_id' => $parent, 'label' => $data['label'], ); if ( false === $wpdb->insert( self::table( 'contexts' ), $row ) ) { return self::error( 'database', __( 'The context could not be saved.', '3task-post-order' ), 503 ); } return self::get( (int) $wpdb->insert_id ); } /** * Delete one order and its history. Content and legacy ranks stay untouched. * * @return array|\WP_Error */ public static function delete( int $id ) { global $wpdb; try { Order::begin(); } catch ( \RuntimeException $error ) { // No transaction of ours is open here, so nothing may be rolled back. return self::error( 'database', __( 'The order could not be deleted. Reload and try again.', '3task-post-order' ), 503 ); } try { $c = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id=%d FOR UPDATE', self::table( 'contexts' ), $id ), ARRAY_A ); if ( ! $c ) { $wpdb->query( 'ROLLBACK' ); return self::error( 'missing', __( 'This order no longer exists.', '3task-post-order' ), 404 ); } if ( 'importing' === $c['scope'] ) { $wpdb->query( 'ROLLBACK' ); return self::error( 'importing', __( 'This order belongs to an unfinished import. Discard the import instead.', '3task-post-order' ), 409 ); } foreach ( array( 'entries', 'history', 'snapshots' ) as $table ) { Order::check( $wpdb->delete( self::table( $table ), array( 'context_id' => $id ) ) ); } Order::check( $wpdb->delete( self::table( 'contexts' ), array( 'id' => $id ) ) ); Order::check( $wpdb->query( 'COMMIT' ) ); } catch ( \RuntimeException $error ) { $wpdb->query( 'ROLLBACK' ); return self::error( 'database', __( 'The order could not be deleted. Reload and try again.', '3task-post-order' ), 503 ); } do_action( 'threepo_order_changed', $id, 0, 0 ); return array( 'deleted' => true, 'id' => $id, ); } /** * @return list> */ public static function contexts(): array { global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY label,id LIMIT 500', self::table( 'contexts' ) ), ARRAY_A ); return array_values( array_filter( array_map( array( self::class, 'cast' ), $rows ), array( self::class, 'allowed' ) ) ); } }