*/ public static function abilities(): array { $taxonomy = ['maxLength' => 32, 'minLength' => 1, 'type' => 'string']; return [ new Ability( 'sarcio/terms-list', __('Look at categories and tags', 'sarcio'), Ability::READ, 'The terms of a taxonomy: category, post_tag, product_cat, product_tag, or any other the site has. Each with id, name, slug, parent and how many posts use it.', ['properties' => ['search' => ['maxLength' => 100, 'type' => 'string'], 'taxonomy' => $taxonomy], 'required' => ['taxonomy'], 'type' => 'object'], static fn (array $args): bool => current_user_can('edit_posts'), static fn (array $args): array => self::list($args), ), new Ability( 'sarcio/terms-save', __('Create or rename a category or tag', 'sarcio'), Ability::WRITE, 'Create a term in a taxonomy, or change the name, slug, parent or description of one by id. Only the fields given change.', [ 'properties' => [ 'description' => ['maxLength' => 2000, 'type' => 'string'], 'id' => ['minimum' => 1, 'type' => 'integer'], 'name' => ['maxLength' => 200, 'type' => 'string'], 'parent' => ['minimum' => 0, 'type' => 'integer'], 'slug' => ['maxLength' => 200, 'type' => 'string'], 'taxonomy' => $taxonomy, ], 'required' => ['taxonomy'], 'type' => 'object', ], static fn (array $args): bool => self::canManage((string) ($args['taxonomy'] ?? '')), static fn (array $args): array => self::save($args), null, static fn (array $before, array $after): array => self::restoreSaved($before, $after), ), new Ability( 'sarcio/terms-remove', __('Remove a category or tag', 'sarcio'), Ability::WRITE, 'Remove a term by id. Posts keep their other terms; a category’s posts fall back to the default category. Undo recreates it and puts its posts back.', ['properties' => ['id' => ['minimum' => 1, 'type' => 'integer'], 'taxonomy' => $taxonomy], 'required' => ['id', 'taxonomy'], 'type' => 'object'], static fn (array $args): bool => self::canManage((string) ($args['taxonomy'] ?? '')), static fn (array $args): array => self::remove($args), null, static fn (array $before, array $after): array => self::recreate($before), ), new Ability( 'sarcio/post-terms-set', __('Put a page, post or product in categories or tags', 'sarcio'), Ability::WRITE, 'Set the terms of a post, page or product for one taxonomy, by name; names that do not exist yet are created. Replaces that taxonomy’s terms on the post; undo puts the previous ones back.', [ 'properties' => [ 'id' => ['minimum' => 1, 'type' => 'integer'], 'taxonomy' => $taxonomy, 'terms' => ['items' => ['maxLength' => 200, 'type' => 'string'], 'maxItems' => 50, 'type' => 'array'], ], 'required' => ['id', 'taxonomy', 'terms'], 'type' => 'object', ], static fn (array $args): bool => isset($args['id']) && current_user_can('edit_post', (int) $args['id']), static fn (array $args): array => self::assign($args), null, static fn (array $before, array $after): array => self::reassign($before), ), ]; } private static function canManage(string $taxonomy): bool { $object = $taxonomy !== '' && function_exists('get_taxonomy') ? get_taxonomy($taxonomy) : null; $capability = is_object($object) && isset($object->cap->manage_terms) ? (string) $object->cap->manage_terms : 'manage_categories'; return current_user_can($capability); } private static function known(string $taxonomy): bool { return $taxonomy !== '' && function_exists('taxonomy_exists') ? taxonomy_exists($taxonomy) : false; } public static function describe(object $term): array { return [ 'count' => (int) ($term->count ?? 0), 'description' => (string) ($term->description ?? ''), 'id' => (int) $term->term_id, 'name' => (string) $term->name, 'parent' => (int) ($term->parent ?? 0), 'slug' => (string) $term->slug, ]; } /** @param array $args */ private static function list(array $args): array { $taxonomy = (string) $args['taxonomy']; if (!self::known($taxonomy)) { return ['error' => __('This site has no taxonomy with that name.', 'sarcio')]; } $query = ['hide_empty' => false, 'number' => 200, 'taxonomy' => $taxonomy]; if (($args['search'] ?? '') !== '') { $query['search'] = (string) $args['search']; } $terms = get_terms($query); return ['terms' => array_map([self::class, 'describe'], is_array($terms) ? array_values(array_filter($terms, 'is_object')) : [])]; } /** @param array $args */ private static function save(array $args): array { $taxonomy = (string) $args['taxonomy']; if (!self::known($taxonomy)) { return ['error' => __('This site has no taxonomy with that name.', 'sarcio')]; } $fields = []; foreach (['name', 'slug', 'description'] as $field) { if (array_key_exists($field, $args)) { $fields[$field] = sanitize_text_field((string) $args[$field]); } } if (array_key_exists('parent', $args)) { $fields['parent'] = (int) $args['parent']; } if (isset($args['id'])) { $term = get_term((int) $args['id'], $taxonomy); if (!is_object($term) || is_wp_error($term)) { return ['error' => __('There is no term with that id.', 'sarcio')]; } if ($fields === []) { return ['error' => __('Nothing would change.', 'sarcio')]; } $before = self::describe($term); $result = wp_update_term((int) $args['id'], $taxonomy, $fields); if (is_wp_error($result)) { return ['error' => sanitize_text_field($result->get_error_message())]; } $after = self::describe(get_term((int) $args['id'], $taxonomy)); return [ 'journal' => [ 'after' => ['created' => false, 'taxonomy' => $taxonomy, 'term' => $after], 'before' => ['created' => false, 'taxonomy' => $taxonomy, 'term' => $before], 'summary' => $before['name'] !== $after['name'] ? sprintf( /* translators: 1: old name, 2: new name. */ __('Renamed “%1$s” to “%2$s”', 'sarcio'), $before['name'], $after['name'], ) : sprintf( /* translators: %s: the term name. */ __('Changed “%s”', 'sarcio'), $after['name'], ), 'target' => 'term:' . $taxonomy . ':' . $args['id'], ], 'term' => $after, ]; } if (($fields['name'] ?? '') === '') { return ['error' => __('A new term needs a name.', 'sarcio')]; } $result = wp_insert_term($fields['name'], $taxonomy, array_diff_key($fields, ['name' => true])); if (is_wp_error($result)) { return ['error' => sanitize_text_field($result->get_error_message())]; } $term = self::describe(get_term((int) $result['term_id'], $taxonomy)); return [ 'journal' => [ 'after' => ['created' => true, 'taxonomy' => $taxonomy, 'term' => $term], 'before' => ['created' => true, 'taxonomy' => $taxonomy, 'term' => null], 'summary' => sprintf( /* translators: 1: the term name, 2: the taxonomy. */ __('Created “%1$s” in %2$s', 'sarcio'), $term['name'], $taxonomy, ), 'target' => 'term:' . $taxonomy . ':' . $term['id'], ], 'term' => $term, ]; } /** * @param array $before * @param array $after */ private static function restoreSaved(array $before, array $after): array { $taxonomy = (string) ($before['taxonomy'] ?? ''); if (!empty($before['created'])) { $id = (int) ($after['term']['id'] ?? 0); if ($id > 0 && is_object(get_term($id, $taxonomy))) { wp_delete_term($id, $taxonomy); } return ['restored' => true]; } $term = $before['term'] ?? null; if (!is_array($term)) { return ['error' => __('Nothing to put back.', 'sarcio')]; } $result = wp_update_term((int) $term['id'], $taxonomy, ['description' => (string) $term['description'], 'name' => (string) $term['name'], 'parent' => (int) $term['parent'], 'slug' => (string) $term['slug']]); return is_wp_error($result) ? ['error' => sanitize_text_field($result->get_error_message())] : ['restored' => true]; } /** @param array $args */ private static function remove(array $args): array { $taxonomy = (string) $args['taxonomy']; $term = self::known($taxonomy) ? get_term((int) $args['id'], $taxonomy) : null; if (!is_object($term) || is_wp_error($term)) { return ['error' => __('There is no term with that id.', 'sarcio')]; } $snapshot = self::describe($term); $objects = get_objects_in_term((int) $args['id'], $taxonomy); $objects = is_array($objects) ? array_map('intval', $objects) : []; $result = wp_delete_term((int) $args['id'], $taxonomy); if ($result !== true) { return ['error' => is_wp_error($result) ? sanitize_text_field($result->get_error_message()) : __('WordPress would not remove that term (it may be the default category).', 'sarcio')]; } return [ 'journal' => [ 'after' => ['removed' => true], 'before' => ['objects' => $objects, 'taxonomy' => $taxonomy, 'term' => $snapshot], 'summary' => sprintf( /* translators: 1: the term name, 2: the taxonomy. */ __('Removed “%1$s” from %2$s', 'sarcio'), $snapshot['name'], $taxonomy, ), 'target' => 'term:' . $taxonomy . ':' . $args['id'], ], 'removed' => $snapshot, ]; } /** @param array $before */ private static function recreate(array $before): array { $taxonomy = (string) ($before['taxonomy'] ?? ''); $term = $before['term'] ?? null; if (!is_array($term) || !self::known($taxonomy)) { return ['error' => __('That term cannot be put back.', 'sarcio')]; } $result = wp_insert_term((string) $term['name'], $taxonomy, ['description' => (string) $term['description'], 'parent' => (int) $term['parent'], 'slug' => (string) $term['slug']]); if (is_wp_error($result)) { return ['error' => sanitize_text_field($result->get_error_message())]; } foreach ((array) ($before['objects'] ?? []) as $objectId) { wp_set_object_terms((int) $objectId, [(int) $result['term_id']], $taxonomy, true); } return ['restored' => true, 'term' => (int) $result['term_id']]; } /** @param array $args */ private static function assign(array $args): array { $taxonomy = (string) $args['taxonomy']; $post = get_post((int) $args['id']); if ($post === null || !self::known($taxonomy)) { return ['error' => __('There is no page, post or product with that id, or no such taxonomy.', 'sarcio')]; } $current = wp_get_object_terms((int) $args['id'], $taxonomy, ['fields' => 'names']); $current = is_array($current) ? array_values(array_map('strval', $current)) : []; $wanted = array_values(array_unique(array_filter(array_map(static fn ($name): string => sanitize_text_field((string) $name), (array) $args['terms']), static fn (string $name): bool => $name !== ''))); if ($wanted === $current) { return ['error' => __('Nothing would change.', 'sarcio')]; } $result = wp_set_object_terms((int) $args['id'], $wanted, $taxonomy, false); if (is_wp_error($result)) { return ['error' => sanitize_text_field($result->get_error_message())]; } return [ 'journal' => [ 'after' => ['id' => (int) $args['id'], 'taxonomy' => $taxonomy, 'terms' => $wanted], 'before' => ['id' => (int) $args['id'], 'taxonomy' => $taxonomy, 'terms' => $current], 'summary' => sprintf( /* translators: 1: the post title, 2: the taxonomy, 3: the terms. */ __('Put “%1$s” in %2$s: %3$s', 'sarcio'), (string) $post->post_title, $taxonomy, $wanted === [] ? __('(none)', 'sarcio') : implode(', ', $wanted), ), 'target' => 'post:' . $args['id'], ], 'terms' => $wanted, ]; } /** @param array $before */ private static function reassign(array $before): array { $result = wp_set_object_terms((int) ($before['id'] ?? 0), array_map('strval', (array) ($before['terms'] ?? [])), (string) ($before['taxonomy'] ?? ''), false); return is_wp_error($result) ? ['error' => sanitize_text_field($result->get_error_message())] : ['restored' => true]; } }