| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* WP-CLI command group for 404 Solution. |
| 9 |
* |
| 10 |
* Registered as: wp abj404 <subcommand> |
| 11 |
*/ |
| 12 |
class ABJ_404_Solution_WPCLICommands extends \WP_CLI_Command { |
| 13 |
|
| 14 |
/** @var ABJ_404_Solution_WPCLIImportExportCommandService|null Injected service; null => container/new fallback. */ |
| 15 |
private $injectedImportExportService; |
| 16 |
|
| 17 |
/** |
| 18 |
* @param ABJ_404_Solution_WPCLIImportExportCommandService|null $importExportService Optional injected |
| 19 |
* import/export application service. WP-CLI instantiates this command class with no constructor |
| 20 |
* arguments, so the parameter is optional and production wiring is unchanged. The seam exists so |
| 21 |
* tests (and any embedder) can drive the import/export workflow against an explicit DataAccess |
| 22 |
* without mutating global container state. |
| 23 |
*/ |
| 24 |
public function __construct($importExportService = null) { |
| 25 |
$this->injectedImportExportService = |
| 26 |
$importExportService instanceof ABJ_404_Solution_WPCLIImportExportCommandService |
| 27 |
? $importExportService |
| 28 |
: null; |
| 29 |
} |
| 30 |
|
| 31 |
/** |
| 32 |
* List redirects. |
| 33 |
* |
| 34 |
* @subcommand list |
| 35 |
* |
| 36 |
* @param array<int, string> $args |
| 37 |
* @param array<string, string> $assocArgs |
| 38 |
* @return void |
| 39 |
*/ |
| 40 |
public function list_redirects($args, $assocArgs) { |
| 41 |
$this->presenter()->present($this->redirectService()->listRedirects( |
| 42 |
$this->assocString($assocArgs, 'status', ''), |
| 43 |
$this->assocString($assocArgs, 'format', 'table') |
| 44 |
)); |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Create a manual redirect. |
| 49 |
* |
| 50 |
* @param array<int, string> $args |
| 51 |
* @param array<string, string> $assocArgs |
| 52 |
* @return void |
| 53 |
*/ |
| 54 |
public function create($args, $assocArgs) { |
| 55 |
$this->presenter()->present($this->redirectService()->createRedirect( |
| 56 |
isset($assocArgs['from']) ? trim((string)$assocArgs['from']) : '', |
| 57 |
isset($assocArgs['to']) ? trim((string)$assocArgs['to']) : '', |
| 58 |
isset($assocArgs['code']) ? (int)$assocArgs['code'] : 301, |
| 59 |
isset($assocArgs['regex']) |
| 60 |
)); |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Move a redirect to the trash. |
| 65 |
* |
| 66 |
* @param array<int, string> $args |
| 67 |
* @param array<string, string> $assocArgs |
| 68 |
* @return void |
| 69 |
*/ |
| 70 |
public function delete($args, $assocArgs) { |
| 71 |
$this->presenter()->present($this->redirectService()->deleteRedirect( |
| 72 |
isset($args[0]) ? trim((string)$args[0]) : '' |
| 73 |
)); |
| 74 |
} |
| 75 |
|
| 76 |
/** |
| 77 |
* Show summary statistics. |
| 78 |
* |
| 79 |
* @param array<int, string> $args |
| 80 |
* @param array<string, string> $assocArgs |
| 81 |
* @return void |
| 82 |
*/ |
| 83 |
public function stats($args, $assocArgs) { |
| 84 |
$container = ABJ_404_Solution_ServiceContainer::getInstance(); |
| 85 |
$statsRepository = null; |
| 86 |
if (!$container->has('stats_repository') && $container->has('data_access')) { |
| 87 |
$candidate = $container->get('data_access'); |
| 88 |
if (is_object($candidate) && method_exists($candidate, 'getStatsDashboardSnapshot')) { |
| 89 |
$statsRepository = $candidate; |
| 90 |
} |
| 91 |
} |
| 92 |
if ($statsRepository === null) { |
| 93 |
$statsRepository = ABJ_404_Solution_StatsRepositoryResolver::resolve(__CLASS__); |
| 94 |
} |
| 95 |
$snapshot = $statsRepository->getStatsDashboardSnapshot(false); |
| 96 |
$data = is_array($snapshot['data']) ? $snapshot['data'] : array(); |
| 97 |
|
| 98 |
/** @var array<string, mixed> $dataAsStrMap */ |
| 99 |
$dataAsStrMap = $data; |
| 100 |
$redirects = isset($dataAsStrMap['redirects']) && is_array($dataAsStrMap['redirects']) ? $dataAsStrMap['redirects'] : array(); |
| 101 |
$captured = isset($dataAsStrMap['captured']) && is_array($dataAsStrMap['captured']) ? $dataAsStrMap['captured'] : array(); |
| 102 |
|
| 103 |
$this->presenter()->present(array( |
| 104 |
'type' => 'format', |
| 105 |
'format' => 'table', |
| 106 |
'rows' => array( |
| 107 |
array('metric' => 'Auto (301)', 'count' => intval($redirects['auto301'] ?? 0)), |
| 108 |
array('metric' => 'Auto (302)', 'count' => intval($redirects['auto302'] ?? 0)), |
| 109 |
array('metric' => 'Manual (301)', 'count' => intval($redirects['manual301'] ?? 0)), |
| 110 |
array('metric' => 'Manual (302)', 'count' => intval($redirects['manual302'] ?? 0)), |
| 111 |
array('metric' => 'Trashed', 'count' => intval($redirects['trashed'] ?? 0)), |
| 112 |
array('metric' => 'Captured 404s', 'count' => intval($captured['captured'] ?? 0)), |
| 113 |
array('metric' => 'Ignored', 'count' => intval($captured['ignored'] ?? 0)), |
| 114 |
array('metric' => 'Captured trash', 'count' => intval($captured['trashed'] ?? 0)), |
| 115 |
), |
| 116 |
'fields' => array('metric', 'count'), |
| 117 |
)); |
| 118 |
} |
| 119 |
|
| 120 |
/** |
| 121 |
* Purge captured 404s. |
| 122 |
* |
| 123 |
* @param array<int, string> $args |
| 124 |
* @param array<string, string> $assocArgs |
| 125 |
* @return void |
| 126 |
*/ |
| 127 |
public function purge($args, $assocArgs) { |
| 128 |
$prepared = $this->redirectService()->preparePurge( |
| 129 |
isset($args[0]) ? strtolower(trim((string)$args[0])) : '' |
| 130 |
); |
| 131 |
if (($prepared['type'] ?? '') !== 'confirm') { |
| 132 |
$this->presenter()->present($prepared); |
| 133 |
return; |
| 134 |
} |
| 135 |
|
| 136 |
$count = isset($prepared['count']) && is_numeric($prepared['count']) ? (int)$prepared['count'] : 0; |
| 137 |
$this->presenter()->confirm( |
| 138 |
"This will permanently delete {$count} captured 404 entr" . ($count === 1 ? 'y' : 'ies') . '. Continue?', |
| 139 |
$assocArgs |
| 140 |
); |
| 141 |
$this->presenter()->present($this->redirectService()->purgeCaptured($count)); |
| 142 |
} |
| 143 |
|
| 144 |
/** |
| 145 |
* Import redirects from a CSV file. |
| 146 |
* |
| 147 |
* @subcommand import |
| 148 |
* |
| 149 |
* @param array<int, string> $args |
| 150 |
* @param array<string, string> $assocArgs |
| 151 |
* @return void |
| 152 |
*/ |
| 153 |
public function import_redirects($args, $assocArgs) { |
| 154 |
$this->presenter()->present($this->importExportService()->importRedirects( |
| 155 |
isset($args[0]) ? (string)$args[0] : '', |
| 156 |
isset($assocArgs['dry-run']) |
| 157 |
)); |
| 158 |
} |
| 159 |
|
| 160 |
/** |
| 161 |
* Export redirects to stdout or a file. |
| 162 |
* |
| 163 |
* @subcommand export |
| 164 |
* |
| 165 |
* @param array<int, string> $args |
| 166 |
* @param array<string, string> $assocArgs |
| 167 |
* @return void |
| 168 |
*/ |
| 169 |
public function export_redirects($args, $assocArgs) { |
| 170 |
$this->presenter()->present($this->importExportService()->exportRedirects( |
| 171 |
$this->assocString($assocArgs, 'format', 'native'), |
| 172 |
isset($assocArgs['output']) ? trim((string)$assocArgs['output']) : '' |
| 173 |
)); |
| 174 |
} |
| 175 |
|
| 176 |
/** |
| 177 |
* Flush one or more internal caches. |
| 178 |
* |
| 179 |
* @subcommand flush-cache |
| 180 |
* |
| 181 |
* @param array<int, string> $args |
| 182 |
* @param array<string, string> $assocArgs |
| 183 |
* @return void |
| 184 |
*/ |
| 185 |
public function flush_cache($args, $assocArgs) { |
| 186 |
$type = $this->assocString($assocArgs, 'type', 'all'); |
| 187 |
$validTypes = array('spelling', 'ngram', 'permalink', 'all'); |
| 188 |
if (!in_array($type, $validTypes, true)) { |
| 189 |
$this->presenter()->present(array('type' => 'error', 'message' => 'Invalid type. Choose one of: ' . implode(', ', $validTypes))); |
| 190 |
return; |
| 191 |
} |
| 192 |
|
| 193 |
$dbCore = abj_service('db_core'); |
| 194 |
$contentRepository = abj_service('content_repository'); |
| 195 |
$flushed = array(); |
| 196 |
|
| 197 |
if ($type === 'spelling' || $type === 'all') { |
| 198 |
$contentRepository->deleteSpellingCache(); |
| 199 |
$flushed[] = 'spelling'; |
| 200 |
} |
| 201 |
|
| 202 |
if ($type === 'permalink' || $type === 'all') { |
| 203 |
$contentRepository->truncatePermalinkCacheTable(); |
| 204 |
$flushed[] = 'permalink'; |
| 205 |
} |
| 206 |
|
| 207 |
if ($type === 'ngram' || $type === 'all') { |
| 208 |
$ngramTable = $dbCore->doTableNameReplacements('{wp_abj404_ngram_cache}'); |
| 209 |
$dbCore->queryAndGetResults( |
| 210 |
"TRUNCATE TABLE `{$ngramTable}`", |
| 211 |
array('skip_repair' => true) |
| 212 |
); |
| 213 |
delete_option('abj404_ngram_cache_initialized'); |
| 214 |
delete_option('abj404_ngram_rebuild_offset'); |
| 215 |
$flushed[] = 'ngram'; |
| 216 |
} |
| 217 |
|
| 218 |
$this->presenter()->present(array('type' => 'success', 'message' => 'Flushed caches: ' . implode(', ', $flushed))); |
| 219 |
} |
| 220 |
|
| 221 |
/** |
| 222 |
* Test which stored redirect would fire for a given URL. |
| 223 |
* |
| 224 |
* @subcommand test |
| 225 |
* |
| 226 |
* @param array<int, string> $args |
| 227 |
* @param array<string, string> $assocArgs |
| 228 |
* @return void |
| 229 |
*/ |
| 230 |
public function test_redirect($args, $assocArgs) { |
| 231 |
$this->presenter()->present($this->redirectService()->testRedirect( |
| 232 |
isset($args[0]) ? trim((string)$args[0]) : '' |
| 233 |
)); |
| 234 |
} |
| 235 |
|
| 236 |
private function presenter(): ABJ_404_Solution_WPCLICommandPresenter { |
| 237 |
$presenter = ABJ_404_Solution_ServiceContainer::safeGet('wpcli_command_presenter'); |
| 238 |
if ($presenter instanceof ABJ_404_Solution_WPCLICommandPresenter) { |
| 239 |
return $presenter; |
| 240 |
} |
| 241 |
return new ABJ_404_Solution_WPCLICommandPresenter(); |
| 242 |
} |
| 243 |
|
| 244 |
private function redirectService(): ABJ_404_Solution_WPCLIRedirectCommandService { |
| 245 |
$service = ABJ_404_Solution_ServiceContainer::safeGet('wpcli_redirect_command_service'); |
| 246 |
if ($service instanceof ABJ_404_Solution_WPCLIRedirectCommandService) { |
| 247 |
return $service; |
| 248 |
} |
| 249 |
return new ABJ_404_Solution_WPCLIRedirectCommandService(); |
| 250 |
} |
| 251 |
|
| 252 |
private function importExportService(): ABJ_404_Solution_WPCLIImportExportCommandService { |
| 253 |
if ($this->injectedImportExportService instanceof ABJ_404_Solution_WPCLIImportExportCommandService) { |
| 254 |
return $this->injectedImportExportService; |
| 255 |
} |
| 256 |
$service = ABJ_404_Solution_ServiceContainer::safeGet('wpcli_import_export_command_service'); |
| 257 |
if ($service instanceof ABJ_404_Solution_WPCLIImportExportCommandService) { |
| 258 |
return $service; |
| 259 |
} |
| 260 |
return new ABJ_404_Solution_WPCLIImportExportCommandService(); |
| 261 |
} |
| 262 |
|
| 263 |
/** |
| 264 |
* @param array<string, string> $assocArgs |
| 265 |
* @param string $key |
| 266 |
* @param string $default |
| 267 |
* @return string |
| 268 |
*/ |
| 269 |
private function assocString(array $assocArgs, string $key, string $default): string { |
| 270 |
return isset($assocArgs[$key]) ? strtolower(trim((string)$assocArgs[$key])) : $default; |
| 271 |
} |
| 272 |
} |
| 273 |
|