*/ public static function abilities(): array { return [ new Ability( 'sarcio/redirects-list', __('Look at redirects', 'sarcio'), Ability::READ, 'The redirects the Redirection plugin holds: id, from, to, status code and whether it is on. Say when the plugin is missing.', ['properties' => ['search' => ['maxLength' => 200, 'type' => 'string']], 'required' => [], 'type' => 'object'], static fn (array $args): bool => current_user_can('manage_options'), static fn (array $args): array => self::list($args), ), new Ability( 'sarcio/redirects-add', __('Add a redirect', 'sarcio'), Ability::WRITE, 'Send one path to another address: from is a path on this site (such as /old-page), to is a path or full URL, code is 301 (permanent, the default) or 302. Needs the Redirection plugin.', [ 'properties' => [ 'code' => ['enum' => [301, 302, 307, 308], 'type' => 'integer'], 'from' => ['maxLength' => 500, 'minLength' => 1, 'type' => 'string'], 'to' => ['maxLength' => 2000, 'minLength' => 1, 'type' => 'string'], ], 'required' => ['from', 'to'], 'type' => 'object', ], static fn (array $args): bool => current_user_can('manage_options'), static fn (array $args): array => self::add($args), null, static fn (array $before, array $after): array => self::remove(['id' => (int) ($after['id'] ?? 0)], true), ), new Ability( 'sarcio/redirects-remove', __('Remove a redirect', 'sarcio'), Ability::WRITE, 'Remove a redirect by its id from redirects-list. Undo puts it back.', ['properties' => ['id' => ['minimum' => 1, 'type' => 'integer']], 'required' => ['id'], 'type' => 'object'], static fn (array $args): bool => current_user_can('manage_options'), static fn (array $args): array => self::remove($args), null, static fn (array $before, array $after): array => self::add(['code' => (int) ($before['code'] ?? 301), 'from' => (string) ($before['from'] ?? ''), 'to' => (string) ($before['to'] ?? '')], true), ), ]; } public static function available(): bool { return class_exists('\Red_Item') && class_exists('\Red_Group'); } private static function missing(): array { return ['error' => __('Redirects need the Redirection plugin (free, on WordPress.org). Install and turn it on first.', 'sarcio')]; } /** @return array> */ private static function rows(): array { global $wpdb; $table = $wpdb->prefix . 'redirection_items'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- the Redirection plugin's own table, read for a listing. $rows = $wpdb->get_results("SELECT id, url, action_data, action_code, status FROM {$table} ORDER BY id DESC LIMIT 500", ARRAY_A); $out = []; foreach (is_array($rows) ? $rows : [] as $row) { $out[] = ['code' => (int) $row['action_code'], 'enabled' => ($row['status'] ?? 'enabled') === 'enabled', 'from' => (string) $row['url'], 'id' => (int) $row['id'], 'to' => (string) $row['action_data']]; } return $out; } /** @param array $args */ private static function list(array $args): array { if (!self::available()) { return self::missing(); } $needle = mb_strtolower(trim((string) ($args['search'] ?? ''))); $rows = array_values(array_filter(self::rows(), static fn (array $row): bool => $needle === '' || str_contains(mb_strtolower($row['from'] . ' ' . $row['to']), $needle))); return ['redirects' => array_slice($rows, 0, 100), 'total' => count($rows)]; } /** @param array $args */ private static function add(array $args, bool $restoring = false): array { if (!self::available()) { return self::missing(); } $from = '/' . ltrim(trim((string) $args['from']), '/'); $to = trim((string) $args['to']); $code = (int) ($args['code'] ?? 301); if ($from === '/' || $to === '') { return ['error' => __('A redirect needs a path to send and an address to send it to.', 'sarcio')]; } foreach (self::rows() as $row) { if ($row['from'] === $from && $row['enabled']) { return ['error' => sprintf( /* translators: 1: the path, 2: where it already goes. */ __('%1$s already redirects to %2$s. Remove that one first.', 'sarcio'), $from, $row['to'], )]; } } $groups = \Red_Group::get_all(); $group = is_array($groups) && $groups !== [] ? (int) (is_object($groups[0]) ? $groups[0]->get_id() : ($groups[0]['id'] ?? 1)) : 1; $item = \Red_Item::create(['action_code' => $code, 'action_data' => ['url' => $to], 'action_type' => 'url', 'group_id' => $group, 'match_type' => 'url', 'url' => $from]); if (is_wp_error($item) || !is_object($item)) { return ['error' => is_wp_error($item) ? sanitize_text_field($item->get_error_message()) : __('Redirection did not accept that redirect.', 'sarcio')]; } $id = (int) $item->get_id(); if ($restoring) { return ['id' => $id, 'restored' => true]; } return [ 'id' => $id, 'journal' => [ 'after' => ['code' => $code, 'from' => $from, 'id' => $id, 'to' => $to], 'before' => [], 'summary' => sprintf( /* translators: 1: the path, 2: the destination. */ __('Redirected %1$s to %2$s', 'sarcio'), $from, $to, ), 'target' => 'redirect:' . $id, ], ]; } /** @param array $args */ private static function remove(array $args, bool $restoring = false): array { if (!self::available()) { return self::missing(); } $id = (int) ($args['id'] ?? 0); $row = null; foreach (self::rows() as $candidate) { if ($candidate['id'] === $id) { $row = $candidate; } } if ($row === null) { return ['error' => __('There is no redirect with that id.', 'sarcio')]; } $item = \Red_Item::get_by_id($id); if (!is_object($item)) { return ['error' => __('There is no redirect with that id.', 'sarcio')]; } $item->delete(); if ($restoring) { return ['restored' => true]; } return [ 'journal' => [ 'after' => ['removed' => true], 'before' => $row, 'summary' => sprintf( /* translators: 1: the path, 2: the destination. */ __('Removed the redirect from %1$s to %2$s', 'sarcio'), $row['from'], $row['to'], ), 'target' => 'redirect:' . $id, ], 'removed' => $row, ]; } }