*/ public static function abilities(): array { return [ new Ability( 'sarcio/changes-list', __('Look at recent changes', 'sarcio'), Ability::READ, 'The last changes the assistant made on this site (id, when, what, whether it was undone), newest first. Use changes-undo with an id to put one back.', ['properties' => ['limit' => ['maximum' => 50, 'minimum' => 1, 'type' => 'integer']], 'required' => [], 'type' => 'object'], static fn (array $args): bool => current_user_can('edit_posts'), static fn (array $args): array => ['changes' => self::list((int) ($args['limit'] ?? 50))], ), new Ability( 'sarcio/changes-undo', __('Undo a change', 'sarcio'), Ability::WRITE, 'Undo one of the assistant’s earlier changes by its id from changes-list. Installing a plugin cannot be undone; everything else can.', ['properties' => ['id' => ['minimum' => 1, 'type' => 'integer']], 'required' => ['id'], 'type' => 'object'], static fn (array $args): bool => current_user_can('edit_posts'), static fn (array $args): array => self::undo((int) $args['id']), static fn (array $args): array => self::undoPreview((int) $args['id']), ), ]; } /** @return array> */ public static function list(int $limit = 50): array { $out = []; foreach (Journal::recent() as $entry) { if (count($out) >= $limit) { break; } if (!self::visible($entry)) { continue; } $ability = Catalog::get($entry['ability']); [$before, $after] = self::changedText($entry['before'], $entry['after']); $out[] = [ 'after' => $after, 'at' => $entry['createdAt'], 'before' => $before, 'by' => self::userName($entry['userId']), 'id' => $entry['id'], 'label' => $ability?->label ?? $entry['ability'], 'summary' => $entry['summary'], 'note' => $entry['undoneAt'] === null ? Catalog::inactiveReason($entry['ability']) : null, 'undoable' => $entry['undoneAt'] === null && $ability !== null && $ability->undo !== null, 'undoneAt' => $entry['undoneAt'], ]; } return $out; } /** * The fields a change touched, one `name: value` line each, as text the * screen can diff. Values that are not text are shown as JSON. * * @param array $before * @param array $after * @return array{0:string,1:string} */ private static function changedText(array $before, array $after): array { $keys = array_unique(array_merge(array_keys($before), array_keys($after))); $changed = array_values(array_filter($keys, static fn ($key) => ($before[$key] ?? null) !== ($after[$key] ?? null))); $line = static function (array $fields, string $key) use ($changed): string { $value = $fields[$key] ?? ''; if ($key === 'builderData' && is_array($value)) { return Builders::textOf($value); } $text = is_scalar($value) || $value === null ? (string) $value : (string) wp_json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (in_array($key, ['content', 'description', 'shortDescription', 'excerpt'], true)) { $text = ContentAbilities::readable($text); } return count($changed) === 1 ? $text : $key . ': ' . $text; }; $side = static function (array $fields) use ($changed, $line): string { $lines = []; foreach ($changed as $key) { if (array_key_exists($key, $fields)) { $lines[] = $line($fields, $key); } } return mb_substr(implode("\n", $lines), 0, 20000); }; return [$side($before), $side($after)]; } private static function undo(int $id): array { $result = Catalog::undo($id); if (!$result['ok']) { return ['error' => (string) ($result['error'] ?? '')]; } return $result['output'] ?? ['undone' => true]; } private static function undoPreview(int $id): array { $entry = Journal::get($id); if ($entry === null) { return ['error' => __('That change is no longer in the list.', 'sarcio')]; } if (!self::visible($entry)) { return ['error' => __('You do not have permission to undo that.', 'sarcio')]; } if ($entry['undoneAt'] !== null) { return ['error' => __('That change was already undone.', 'sarcio')]; } $ability = Catalog::get($entry['ability']); if ($ability === null || $ability->undo === null) { return ['error' => Catalog::inactiveReason($entry['ability']) ?? __('That change cannot be undone from here.', 'sarcio')]; } return [ 'after' => '', 'before' => $entry['summary'], 'summary' => sprintf( /* translators: %s: what the change did. */ __('Undo: %s', 'sarcio'), $entry['summary'], ), ]; } /** * Whether the signed-in user may see a change: an administrator sees them * all, anyone else the changes they could have made themselves, and a * change by an ability no longer here only its author. * * @param array{ability:string,before:array,after:array,userId:int} $entry */ public static function visible(array $entry): bool { if (current_user_can('manage_options')) { return true; } $ability = $entry['ability'] === 'sarcio/changes-undo' ? null : Catalog::get($entry['ability']); if ($ability === null) { return $entry['userId'] === get_current_user_id(); } return $ability->allowed($entry['before'] + $entry['after']); } private static function userName(int $userId): string { $user = function_exists('get_userdata') ? get_userdata($userId) : null; return is_object($user) && isset($user->display_name) ? (string) $user->display_name : ''; } }