}>}> */ public function types(): array { $out = []; foreach (get_post_types(['public' => true, 'show_ui' => true], 'objects') as $type) { if (!$this->allowed($type->name)) { continue; } $out[] = ['name' => $type->name, 'label' => (string) $type->labels->singular_name, 'block_editor' => self::blockEditor($type->name), 'taxonomies' => $this->taxonomies($type->name)]; } usort($out, static fn (array $a, array $b): int => [$a['name'] !== 'post', $a['name'] !== 'page', $a['label']] <=> [$b['name'] !== 'post', $b['name'] !== 'page', $b['label']]); return $out; } /** * May the person create this type of draft from Studio? * * @since 0.1.4 */ public function allowed(string $type): bool { if (!self::validType($type)) { return false; } $object = get_post_type_object($type); return $object instanceof \WP_Post_Type && $type !== 'attachment' && $object->public && $object->show_ui && post_type_supports($type, 'editor') && ($type === 'post' || $type === 'page' || $object->show_in_rest) && current_user_can($object->cap->create_posts) && current_user_can($object->cap->edit_posts); } /** A registered WordPress post type slug (maximum 20 characters). */ public static function validType(string $type): bool { return preg_match('/^[a-z0-9_-]{1,20}$/D', $type) === 1; } /** Whether this person can create at least one supported Studio target. */ public function canCreateAny(): bool { foreach (get_post_types(['public' => true, 'show_ui' => true], 'objects') as $type) { if ($this->allowed($type->name)) { return true; } } return false; } /** Whether this person can edit drafts in at least one visible content type. */ public function canEditAny(): bool { foreach (get_post_types(['show_ui' => true], 'objects') as $type) { if ($type instanceof \WP_Post_Type && current_user_can($type->cap->edit_posts)) { return true; } } return false; } /** * Does the block editor open for this type? (Not loaded in a REST request, so load it the way core does.) * * @since 0.1.4 */ public static function blockEditor(string $type): bool { if (!function_exists('use_block_editor_for_post_type')) { require_once ABSPATH . 'wp-admin/includes/post.php'; } return use_block_editor_for_post_type($type); } /** * The terms the person asked for, cleaned to what `wp_insert_post( tax_input )` takes: ids for a hierarchical * taxonomy (only terms that exist), names for a flat one. Taxonomies the person may not assign are dropped. * * @since 0.1.4 * @param array $raw * @return array> */ public function terms(string $type, array $raw): array { $out = []; foreach ($this->taxonomyObjects($type) as $taxonomy) { $asked = $raw[$taxonomy->name] ?? null; if (!is_array($asked) || $asked === [] || !current_user_can($taxonomy->cap->assign_terms)) { continue; } $clean = []; foreach (array_slice($asked, 0, 20) as $term) { if ($taxonomy->hierarchical) { $id = (int) $term; if ($id > 0 && term_exists($id, $taxonomy->name)) { $clean[] = $id; } } elseif (is_scalar($term)) { $name = mb_substr(trim(sanitize_text_field((string) $term)), 0, 60); if ($name !== '') { $clean[] = $name; } } } if ($clean !== []) { $out[$taxonomy->name] = array_values(array_unique($clean)); } } return $out; } /** * An image the person may use as the featured image: it exists, is an image, and they may edit it. * * @since 0.1.4 */ public function image(int $id): bool { return $id > 0 && get_post_type($id) === 'attachment' && wp_attachment_is_image($id) && current_user_can('edit_post', $id); } /** * Posts and pages (any type the person may use) matching a search, for "Improve an existing post". * * @since 0.1.4 * @return list */ public function posts(string $search, int $limit = 30, string $type = ''): array { $types = array_values(array_filter(array_map(static fn (array $t): string => $t['name'], $this->types()))); if ($type !== '') { if (!in_array($type, $types, true)) { return []; } $types = [$type]; } if ($types === []) { return []; } $out = []; foreach (get_posts([ 'post_type' => $types, 'post_status' => ['publish', 'draft', 'pending', 'future', 'private'], 'posts_per_page' => max(1, min(50, $limit)), 's' => $search, 'orderby' => $search === '' ? 'modified' : 'relevance', 'order' => 'DESC', ]) as $post) { if ($post instanceof \WP_Post && current_user_can('edit_post', $post->ID)) { $out[] = ['id' => $post->ID, 'title' => $post->post_title !== '' ? $post->post_title : __('(no title)', 'maze-graph'), 'type' => $post->post_type, 'status' => $post->post_status]; } } return $out; } /** * Images the person may use, newest first. * * @since 0.1.4 * @return list */ public function media(string $search, int $limit = 30): array { $out = []; foreach (get_posts([ 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'image', 'posts_per_page' => max(1, min(50, $limit)), 's' => $search, 'orderby' => 'date', 'order' => 'DESC', ]) as $media) { if ($media instanceof \WP_Post && current_user_can('edit_post', $media->ID)) { $out[] = ['id' => $media->ID, 'title' => $media->post_title !== '' ? $media->post_title : basename((string) get_attached_file($media->ID)), 'url' => (string) wp_get_attachment_image_url($media->ID, 'thumbnail')]; } } return $out; } /** * @return list<\WP_Taxonomy> */ private function taxonomyObjects(string $type): array { return array_values(array_filter(get_object_taxonomies($type, 'objects'), static fn ($t): bool => $t instanceof \WP_Taxonomy && $t->show_ui && $t->show_in_rest)); } /** * @return list}> */ private function taxonomies(string $type): array { $out = []; foreach ($this->taxonomyObjects($type) as $taxonomy) { if (!current_user_can($taxonomy->cap->assign_terms)) { continue; } $terms = []; if ($taxonomy->hierarchical) { $found = get_terms(['taxonomy' => $taxonomy->name, 'hide_empty' => false, 'number' => self::MAX_TERMS, 'orderby' => 'name']); foreach (is_array($found) ? $found : [] as $term) { if ($term instanceof \WP_Term) { $terms[] = ['id' => $term->term_id, 'name' => $term->name]; } } } $out[] = ['name' => $taxonomy->name, 'label' => (string) $taxonomy->labels->name, 'hierarchical' => (bool) $taxonomy->hierarchical, 'terms' => $terms]; } return $out; } }