*/
public static function abilities(): array
{
$idSchema = ['minimum' => 1, 'type' => 'integer'];
$fieldSchemas = [
'content' => ['description' => 'Block markup or HTML for the whole content', 'type' => 'string'],
'excerpt' => ['type' => 'string'],
'parent' => ['minimum' => 0, 'type' => 'integer'],
'slug' => ['maxLength' => 200, 'type' => 'string'],
'status' => ['enum' => self::STATUSES, 'type' => 'string'],
'title' => ['maxLength' => 500, 'type' => 'string'],
];
return [
new Ability(
'sarcio/content-search',
__('Look for pages and posts', 'sarcio'),
Ability::READ,
'Search pages, posts and products by words in their title or content. Returns id, title, type, status, url, last modified and a short excerpt. Use it to find the id of what to change. Type wp_block is a synced pattern (a reusable block), searched only when asked for by type',
[
'properties' => [
'limit' => ['maximum' => 20, 'minimum' => 1, 'type' => 'integer'],
'query' => ['maxLength' => 200, 'type' => 'string'],
'status' => ['enum' => ['any', 'publish', 'draft', 'pending', 'private'], 'type' => 'string'],
'type' => ['enum' => ['any', 'page', 'post', 'product', 'wp_block'], 'type' => 'string'],
],
'required' => [],
'type' => 'object',
],
static fn (array $args): bool => current_user_can('edit_posts'),
static fn (array $args): array => self::search($args),
),
new Ability(
'sarcio/content-get',
__('Read a page or post', 'sarcio'),
Ability::READ,
'Read one page, post or product by id: title, status, slug, type, parent, template, excerpt and the full content as block markup or HTML. A page built with Elementor says builder: "elementor" and carries its text as builderText, one widget per line; a Divi page says builder: "divi" with its words in builderText; content-replace changes that text on both. Read before you change text, so the search string is exact.',
['properties' => ['id' => $idSchema], 'required' => ['id'], 'type' => 'object'],
static fn (array $args): bool => isset($args['id']) ? current_user_can('edit_post', (int) $args['id']) : current_user_can('edit_posts'),
static fn (array $args): array => self::get((int) $args['id']),
),
new Ability(
'sarcio/patterns-list',
__('List the theme’s layouts', 'sarcio'),
Ability::READ,
'The block patterns this site offers (name, title, categories), for building a new page from the theme’s own layouts with content-create.',
['properties' => [], 'type' => 'object'],
static fn (array $args): bool => current_user_can('edit_pages'),
static fn (array $args): array => self::patterns(),
),
new Ability(
'sarcio/content-update',
__('Change a page or post', 'sarcio'),
Ability::WRITE,
'Change the title, content, excerpt, slug, status or parent of an existing page, post or product. Only the fields given change. WordPress keeps a revision. Prefer content-replace for a small text change.',
['properties' => ['id' => $idSchema] + $fieldSchemas, 'required' => ['id'], 'type' => 'object'],
static fn (array $args): bool => self::canEdit($args),
static fn (array $args): array => self::update($args),
null,
static fn (array $before, array $after): array => self::restore($before),
false,
static fn (array $args, Overlay $overlay): array => self::stageUpdate($args, $overlay),
),
new Ability(
'sarcio/content-replace',
__('Change text on a page', 'sarcio'),
Ability::WRITE,
'Replace an exact piece of text inside the content of a page, post or product, or inside the widgets of a page built with Elementor. Refused when the text is not found, or found more than once unless all is true. Read the content first and copy the text exactly.',
[
'properties' => [
'all' => ['type' => 'boolean'],
'id' => $idSchema,
'replace' => ['maxLength' => 20000, 'type' => 'string'],
'search' => ['maxLength' => 20000, 'minLength' => 1, 'type' => 'string'],
],
'required' => ['id', 'search', 'replace'],
'type' => 'object',
],
static fn (array $args): bool => self::canEdit($args),
static fn (array $args): array => self::replace($args),
null,
static fn (array $before, array $after): array => self::restore($before),
false,
static fn (array $args, Overlay $overlay): array => self::stageReplace($args, $overlay),
),
new Ability(
'sarcio/content-create',
__('Create a page or post', 'sarcio'),
Ability::WRITE,
'Create a new page or post. Give content as block markup or HTML, or patterns (names from patterns-list) to build it from the theme’s layouts. Saved as a draft unless status says otherwise.',
[
'properties' => [
'content' => ['type' => 'string'],
'parent' => ['minimum' => 0, 'type' => 'integer'],
'patterns' => ['items' => ['maxLength' => 200, 'type' => 'string'], 'maxItems' => 20, 'type' => 'array'],
'slug' => ['maxLength' => 200, 'type' => 'string'],
'status' => ['enum' => ['draft', 'publish', 'pending', 'private'], 'type' => 'string'],
'title' => ['maxLength' => 500, 'minLength' => 1, 'type' => 'string'],
'type' => ['enum' => ['page', 'post'], 'type' => 'string'],
],
'required' => ['type', 'title'],
'type' => 'object',
],
static fn (array $args): bool => self::canCreate($args),
static fn (array $args): array => self::create($args),
static fn (array $args): array => self::createPreview($args),
static fn (array $before, array $after): array => self::trash($after),
),
new Ability(
'sarcio/content-publish-at',
__('Publish a page or post at a time', 'sarcio'),
Ability::WRITE,
'Schedule a draft page, post or product to publish itself at a date and time in the site’s time zone (at: YYYY-MM-DDTHH:MM), or move an already scheduled one. WordPress publishes it then. Undo puts the previous status and date back.',
['properties' => ['at' => ['pattern' => '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$', 'type' => 'string'], 'id' => ['minimum' => 1, 'type' => 'integer']], 'required' => ['at', 'id'], 'type' => 'object'],
static fn (array $args): bool => self::canEdit($args),
static fn (array $args): array => self::publishAt($args),
null,
static fn (array $before, array $after): array => self::restoreTiming($before),
),
];
}
/**
* @return array{content?:string,count:int,error?:string}
*/
public static function replaceText(string $content, string $search, string $replace, bool $all): array
{
$replace = wp_kses_post($replace);
$count = $search === '' ? 0 : substr_count($content, $search);
if ($count === 0) {
return ['count' => 0, 'error' => __('That text was not found on the page. Read the page and copy the text exactly.', 'sarcio')];
}
if ($count > 1 && !$all) {
return ['count' => $count, 'error' => sprintf(
/* translators: %d: how many times the text appears. */
__('That text appears %d times on the page. Say which one, or ask to change all of them.', 'sarcio'),
$count,
)];
}
return ['content' => str_replace($search, $replace, $content), 'count' => $count];
}
/**
* Page content as the owner reads it: block comments and tags removed, one
* paragraph per line, so a change shows as words rather than markup.
*/
public static function readable(string $content): string
{
$text = (string) preg_replace('//s', '', $content);
$text = (string) preg_replace('#(p|h[1-6]|li|div|blockquote|figcaption|pre|td|th|tr)>|
#i', "\n", $text);
$text = html_entity_decode(wp_strip_all_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$lines = array_filter(array_map('trim', explode("\n", $text)), static fn (string $line): bool => $line !== '');
return mb_substr(implode("\n", $lines), 0, 20000);
}
/** @return array */
public static function snapshot(object $post): array
{
return [
'content' => (string) $post->post_content,
'excerpt' => (string) $post->post_excerpt,
'id' => (int) $post->ID,
'parent' => (int) $post->post_parent,
'slug' => (string) $post->post_name,
'status' => (string) $post->post_status,
'title' => (string) $post->post_title,
'type' => (string) $post->post_type,
];
}
/** @param array $args */
private static function canEdit(array $args): bool
{
$id = (int) ($args['id'] ?? 0);
if ($id <= 0) {
return current_user_can('edit_posts');
}
if (!current_user_can('edit_post', $id)) {
return false;
}
if (($args['status'] ?? null) === 'publish') {
$type = get_post_type($id);
return current_user_can($type === 'page' ? 'publish_pages' : 'publish_posts');
}
return true;
}
/** @param array $args */
private static function canCreate(array $args): bool
{
$page = ($args['type'] ?? 'page') === 'page';
if (!current_user_can($page ? 'edit_pages' : 'edit_posts')) {
return false;
}
if (($args['status'] ?? 'draft') === 'publish') {
return current_user_can($page ? 'publish_pages' : 'publish_posts');
}
return true;
}
private static function filterContent(string $content): string
{
return wp_kses_post($content);
}
private static function keepContent(string $content): string
{
return current_user_can('unfiltered_html') ? $content : wp_kses_post($content);
}
private static function summarize(object $post): array
{
return [
'excerpt' => mb_substr(wp_strip_all_tags((string) ($post->post_excerpt !== '' ? $post->post_excerpt : $post->post_content)), 0, 200),
'id' => (int) $post->ID,
'modified' => (string) $post->post_modified_gmt,
'status' => (string) $post->post_status,
'title' => (string) $post->post_title,
'type' => (string) $post->post_type,
'url' => (string) get_permalink($post),
];
}
/** @param array $args */
private static function search(array $args): array
{
$type = (string) ($args['type'] ?? 'any');
$types = $type === 'any' ? array_values(array_filter(self::TYPES, static fn (string $one): bool => $one !== 'wp_block' && ($one !== 'product' || SiteAbilities::woocommerceActive()))) : [$type];
$query = [
'numberposts' => (int) ($args['limit'] ?? 10),
'orderby' => isset($args['query']) && $args['query'] !== '' ? 'relevance' : 'modified',
'post_status' => (string) ($args['status'] ?? 'any'),
'post_type' => $types,
's' => (string) ($args['query'] ?? ''),
];
$posts = is_array(get_posts($query)) ? get_posts($query) : [];
if (Stage::active()) {
$overlay = Stage::overlay();
$posts = array_map(static fn (object $post): object => $overlay->applyToPost($post), $posts);
}
return ['results' => array_map([self::class, 'summarize'], $posts)];
}
private static function get(int $id): array
{
$post = Stage::post($id);
if ($post === null || !in_array((string) $post->post_type, self::TYPES, true)) {
return ['error' => __('There is no page or post with that id.', 'sarcio')];
}
$content = (string) $post->post_content;
$truncated = mb_strlen($content) > self::MAX_CONTENT_CHARS;
$overlay = Stage::active() ? Stage::overlay() : null;
$builder = Builders::of((int) $post->ID, $overlay);
return self::snapshot($post) + [
'template' => (string) get_page_template_slug($post),
'truncated' => $truncated,
'url' => (string) get_permalink($post),
] + ($truncated ? ['content' => mb_substr($content, 0, self::MAX_CONTENT_CHARS)] : [])
+ ($builder === Builders::ELEMENTOR ? ['builder' => $builder, 'builderText' => Builders::text((int) $post->ID, $overlay)] : [])
+ ($builder === Builders::DIVI ? ['builder' => $builder, 'builderText' => Builders::diviText($content)] : []);
}
private static function patterns(): array
{
if (!class_exists('WP_Block_Patterns_Registry')) {
return ['patterns' => []];
}
$all = \WP_Block_Patterns_Registry::get_instance()->get_all_registered();
$out = [];
foreach (array_slice($all, 0, 60) as $pattern) {
$out[] = [
'categories' => array_values((array) ($pattern['categories'] ?? [])),
'name' => (string) ($pattern['name'] ?? ''),
'title' => (string) ($pattern['title'] ?? ''),
];
}
return ['patterns' => $out];
}
/** @param array $args */
private static function changes(array $args, object $post): array
{
$before = self::snapshot($post);
$after = $before;
foreach (self::FIELDS as $field) {
if (array_key_exists($field, $args)) {
$after[$field] = match ($field) {
'content' => self::filterContent((string) $args[$field]),
'excerpt' => wp_kses_post((string) $args[$field]),
'title' => sanitize_text_field((string) $args[$field]),
default => $args[$field],
};
}
}
return [$before, $after];
}
/** @param array $args */
private static function update(array $args): array
{
$post = get_post((int) $args['id']);
if ($post === null) {
return ['error' => __('There is no page or post with that id.', 'sarcio')];
}
[$before, $after] = self::changes($args, $post);
if ($before === $after) {
return ['error' => __('Nothing would change.', 'sarcio')];
}
$result = self::write($after);
if (isset($result['error'])) {
return $result;
}
$changed = array_keys(array_filter($after, static fn ($value, $key) => $before[$key] !== $value, ARRAY_FILTER_USE_BOTH));
return [
'changed' => $changed,
'id' => (int) $post->ID,
'journal' => [
'after' => $after,
'before' => $before,
'summary' => sprintf(
/* translators: 1: the fields changed, 2: the page title. */
__('Changed %1$s of “%2$s”', 'sarcio'),
implode(', ', $changed),
$before['title'],
),
'target' => 'post:' . $post->ID,
],
'url' => (string) get_permalink($post),
];
}
/** @param array $args */
private static function stageUpdate(array $args, Overlay $overlay): array
{
$post = $overlay->post((int) $args['id']);
if ($post === null) {
return ['error' => __('There is no page or post with that id.', 'sarcio')];
}
[$before, $after] = self::changes($args, $post);
$changed = array_keys(array_filter($after, static fn ($value, $key) => $before[$key] !== $value, ARRAY_FILTER_USE_BOTH));
if ($changed === []) {
return ['error' => __('Nothing would change.', 'sarcio')];
}
$overlay->setPost((int) $post->ID, self::postFields($after));
return [
'after' => self::describe($after, $changed),
'before' => self::describe($before, $changed),
'summary' => sprintf(
/* translators: 1: the fields changed, 2: the page title. */
__('Change the %1$s of “%2$s”', 'sarcio'),
implode(', ', $changed),
$before['title'],
),
];
}
/** @param array $args */
private static function replace(array $args): array
{
$post = get_post((int) $args['id']);
if ($post === null) {
return ['error' => __('There is no page or post with that id.', 'sarcio')];
}
if (Builders::of((int) $post->ID) === Builders::ELEMENTOR) {
return self::replaceInBuilder($post, $args);
}
$result = self::replaceText((string) $post->post_content, (string) $args['search'], (string) $args['replace'], (bool) ($args['all'] ?? false));
if (isset($result['error'])) {
return ['error' => $result['error']];
}
$before = self::snapshot($post);
$after = ['content' => self::keepContent($result['content'])] + $before;
$written = self::write($after);
if (isset($written['error'])) {
return $written;
}
return [
'id' => (int) $post->ID,
'journal' => [
'after' => $after,
'before' => $before,
'summary' => sprintf(
/* translators: 1: the old text, 2: the new text, 3: the page title. */
__('Changed “%1$s” to “%2$s” on “%3$s”', 'sarcio'),
mb_substr((string) $args['search'], 0, 60),
mb_substr((string) $args['replace'], 0, 60),
$before['title'],
),
'target' => 'post:' . $post->ID,
],
'replaced' => $result['count'],
'url' => (string) get_permalink($post),
];
}
/** @param array $args */
private static function stageReplace(array $args, Overlay $overlay): array
{
$post = $overlay->post((int) $args['id']);
if ($post === null) {
return ['error' => __('There is no page or post with that id.', 'sarcio')];
}
if (Builders::of((int) $post->ID, $overlay) === Builders::ELEMENTOR) {
$before = Builders::text((int) $post->ID, $overlay);
$result = Builders::replace((int) $post->ID, (string) $args['search'], (string) $args['replace'], (bool) ($args['all'] ?? false), $overlay);
if (isset($result['error'])) {
return ['error' => $result['error']];
}
$overlay->setMeta((int) $post->ID, Builders::ELEMENTOR_DATA, Builders::encode($result['data']));
$overlay->setMeta((int) $post->ID, Builders::ELEMENTOR_CACHE, '');
return [
'after' => Builders::text((int) $post->ID, $overlay),
'before' => $before,
'summary' => self::replaceSummary($result['count'], (string) $post->post_title),
];
}
$result = self::replaceText((string) $post->post_content, (string) $args['search'], (string) $args['replace'], (bool) ($args['all'] ?? false));
if (isset($result['error'])) {
return ['error' => $result['error']];
}
$overlay->setPost((int) $post->ID, ['post_content' => self::keepContent((string) $result['content'])]);
return [
'after' => self::readable((string) $result['content']),
'before' => self::readable((string) $post->post_content),
'summary' => self::replaceSummary($result['count'], (string) $post->post_title),
];
}
private static function replaceSummary(int $count, string $title): string
{
return $count > 1
? sprintf(
/* translators: 1: how many times, 2: the page title. */
__('Change this text in %1$d places on “%2$s”', 'sarcio'),
$count,
$title,
)
: sprintf(
/* translators: %s: the page title. */
__('Change this text on “%s”', 'sarcio'),
$title,
);
}
/**
* The same exact-text change inside a page builder's data, journaled with
* the builder's whole tree so undo puts every widget back.
*
* @param array $args
*/
private static function replaceInBuilder(object $post, array $args): array
{
$id = (int) $post->ID;
$before = Builders::data($id) ?? [];
$result = Builders::replace($id, (string) $args['search'], (string) $args['replace'], (bool) ($args['all'] ?? false));
if (isset($result['error'])) {
return ['error' => $result['error']];
}
if (!Builders::write($id, $result['data'])) {
return ['error' => __('WordPress could not save the change.', 'sarcio')];
}
return [
'builder' => Builders::ELEMENTOR,
'id' => $id,
'journal' => [
'after' => ['builder' => Builders::ELEMENTOR, 'builderData' => $result['data'], 'id' => $id, 'title' => (string) $post->post_title],
'before' => ['builder' => Builders::ELEMENTOR, 'builderData' => $before, 'id' => $id, 'title' => (string) $post->post_title],
'summary' => sprintf(
/* translators: 1: the old text, 2: the new text, 3: the page title. */
__('Changed “%1$s” to “%2$s” on “%3$s”', 'sarcio'),
mb_substr((string) $args['search'], 0, 60),
mb_substr((string) $args['replace'], 0, 60),
(string) $post->post_title,
),
'target' => 'post:' . $id,
],
'replaced' => $result['count'],
'url' => (string) get_permalink($post),
];
}
/** @param array $args */
private static function create(array $args): array
{
$content = self::composeContent($args);
if (isset($content['error'])) {
return $content;
}
$data = [
'post_content' => self::filterContent($content['content']),
'post_name' => (string) ($args['slug'] ?? ''),
'post_parent' => (int) ($args['parent'] ?? 0),
'post_status' => (string) ($args['status'] ?? 'draft'),
'post_title' => sanitize_text_field((string) $args['title']),
'post_type' => (string) $args['type'],
];
$id = wp_insert_post($data, true);
if (is_wp_error($id) || !is_int($id) || $id <= 0) {
return ['error' => __('WordPress could not create it.', 'sarcio')];
}
return [
'id' => $id,
'journal' => [
'after' => ['id' => $id, 'status' => $data['post_status'], 'title' => $data['post_title'], 'type' => $data['post_type']],
'before' => [],
'summary' => sprintf(
/* translators: 1: page or post, 2: its title. */
__('Created the %1$s “%2$s”', 'sarcio'),
$data['post_type'],
$data['post_title'],
),
'target' => 'post:' . $id,
],
'status' => $data['post_status'],
'url' => (string) get_permalink($id),
];
}
/** @param array $args */
private static function createPreview(array $args): array
{
$content = self::composeContent($args);
if (isset($content['error'])) {
return $content;
}
$status = (string) ($args['status'] ?? 'draft');
return [
'after' => mb_substr(wp_strip_all_tags($content['content']), 0, 1500),
'before' => '',
'summary' => sprintf(
/* translators: 1: page or post, 2: its title, 3: draft or publish. */
__('Create the %1$s “%2$s” (%3$s)', 'sarcio'),
(string) $args['type'],
(string) $args['title'],
$status === 'publish' ? __('published right away', 'sarcio') : __('as a draft', 'sarcio'),
),
];
}
/**
* @param array $args
* @return array{content?:string,error?:string}
*/
private static function composeContent(array $args): array
{
$content = (string) ($args['content'] ?? '');
$patterns = (array) ($args['patterns'] ?? []);
if ($patterns === []) {
return ['content' => $content];
}
if (!class_exists('WP_Block_Patterns_Registry')) {
return ['error' => __('This site has no layouts to build from.', 'sarcio')];
}
$registry = \WP_Block_Patterns_Registry::get_instance();
$parts = [];
foreach ($patterns as $name) {
$pattern = $registry->get_registered((string) $name);
if ($pattern === null) {
return ['error' => sprintf(
/* translators: %s: the layout name. */
__('There is no layout called “%s”.', 'sarcio'),
(string) $name,
)];
}
$parts[] = (string) ($pattern['content'] ?? '');
}
$parts[] = $content;
return ['content' => implode("\n\n", array_filter($parts, static fn (string $part): bool => $part !== ''))];
}
/**
* @param array $fields the snapshot shape
* @return array WP_Post field names
*/
private static function postFields(array $fields): array
{
return [
'post_content' => (string) $fields['content'],
'post_excerpt' => (string) $fields['excerpt'],
'post_name' => (string) $fields['slug'],
'post_parent' => (int) $fields['parent'],
'post_status' => (string) $fields['status'],
'post_title' => (string) $fields['title'],
];
}
/** @param array $args */
private static function publishAt(array $args): array
{
$post = get_post((int) $args['id']);
if ($post === null) {
return ['error' => __('There is no page or post with that id.', 'sarcio')];
}
if ((string) $post->post_status === 'publish') {
return ['error' => __('That is already published. Unpublish it first if you want it to go out later.', 'sarcio')];
}
$timestamp = Scheduled::parse((string) $args['at']);
if ($timestamp === null) {
return ['error' => __('That is not a date and time I can read (use YYYY-MM-DDTHH:MM).', 'sarcio')];
}
if ($timestamp <= time()) {
return ['error' => __('That time has already passed; use content-update with status publish to publish now.', 'sarcio')];
}
$before = ['date' => (string) $post->post_date, 'dateGmt' => (string) ($post->post_date_gmt ?? ''), 'id' => (int) $post->ID, 'status' => (string) $post->post_status];
$result = wp_update_post(['ID' => (int) $post->ID, 'post_date' => wp_date('Y-m-d H:i:s', $timestamp, wp_timezone()), 'post_date_gmt' => gmdate('Y-m-d H:i:s', $timestamp), 'post_status' => 'future'], true);
if (is_wp_error($result) || (int) $result <= 0) {
return ['error' => __('WordPress could not schedule it.', 'sarcio')];
}
$when = wp_date('Y-m-d H:i', $timestamp, wp_timezone());
return [
'journal' => [
'after' => ['at' => $when, 'id' => (int) $post->ID, 'status' => 'future'],
'before' => $before,
'summary' => sprintf(
/* translators: 1: the page title, 2: the date and time. */
__('Scheduled “%1$s” to publish at %2$s', 'sarcio'),
(string) $post->post_title,
$when,
),
'target' => 'post:' . $post->ID,
],
'publishesAt' => $when,
];
}
/** @param array $before */
private static function restoreTiming(array $before): array
{
$id = (int) ($before['id'] ?? 0);
if ($id <= 0 || get_post($id) === null) {
return ['error' => __('That page no longer exists.', 'sarcio')];
}
$data = ['ID' => $id, 'post_status' => (string) ($before['status'] ?? 'draft')];
if (($before['date'] ?? '') !== '') {
$data['post_date'] = (string) $before['date'];
$data['post_date_gmt'] = (string) ($before['dateGmt'] ?? '');
}
$result = wp_update_post($data, true);
return is_wp_error($result) ? ['error' => sanitize_text_field($result->get_error_message())] : ['restored' => true];
}
/** @param array $fields */
private static function write(array $fields): array
{
$result = wp_update_post([
'ID' => (int) $fields['id'],
'post_content' => (string) $fields['content'],
'post_excerpt' => (string) $fields['excerpt'],
'post_name' => (string) $fields['slug'],
'post_parent' => (int) $fields['parent'],
'post_status' => (string) $fields['status'],
'post_title' => (string) $fields['title'],
], true);
if (is_wp_error($result) || (int) $result <= 0) {
return ['error' => __('WordPress could not save the change.', 'sarcio')];
}
if (Builders::of((int) $result) === Builders::DIVI) {
Builders::clearDiviCache((int) $result);
}
return ['id' => (int) $result];
}
/** @param array $before */
private static function restore(array $before): array
{
if (!isset($before['id']) || get_post((int) $before['id']) === null) {
return ['error' => __('That page no longer exists.', 'sarcio')];
}
if (($before['builder'] ?? null) === Builders::ELEMENTOR && is_array($before['builderData'] ?? null)) {
if (!Builders::write((int) $before['id'], $before['builderData'])) {
return ['error' => __('WordPress could not restore the page.', 'sarcio')];
}
return ['id' => (int) $before['id'], 'restored' => true];
}
$written = self::write($before);
if (isset($written['error'])) {
return $written;
}
return ['id' => (int) $before['id'], 'restored' => true];
}
/** @param array $after */
private static function trash(array $after): array
{
$id = (int) ($after['id'] ?? 0);
if ($id <= 0 || get_post($id) === null) {
return ['error' => __('That page no longer exists.', 'sarcio')];
}
$result = wp_trash_post($id);
if (!$result) {
return ['error' => __('WordPress could not move it to the bin.', 'sarcio')];
}
return ['id' => $id, 'trashed' => true];
}
/**
* @param array $fields
* @param array $changed
*/
private static function describe(array $fields, array $changed): string
{
$lines = [];
foreach ($changed as $field) {
$value = (string) $fields[$field];
if (in_array($field, ['content', 'excerpt'], true)) {
$value = self::readable($value);
}
$lines[] = count($changed) === 1 ? $value : $field . ': ' . $value;
}
return mb_substr(implode("\n", $lines), 0, 20000);
}
}