|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Cleantalk\ApbctWP\Firewall; |
| 4 | + |
| 5 | +use Cleantalk\ApbctWP\HTTP\HTTPMultiRequestService; |
| 6 | +use Cleantalk\Common\TT; |
| 7 | + |
| 8 | +/** |
| 9 | + * Class SFWFilesDownloader |
| 10 | + * |
| 11 | + * Handles downloading of SpamFireWall data files from remote servers. |
| 12 | + * Manages batch processing, retry logic, and error handling for file downloads. |
| 13 | + */ |
| 14 | +class SFWFilesDownloader |
| 15 | +{ |
| 16 | + /** |
| 17 | + * HTTP multi-request service instance |
| 18 | + * |
| 19 | + * @var HTTPMultiRequestService |
| 20 | + */ |
| 21 | + private $http_multi_request_service; |
| 22 | + |
| 23 | + /** |
| 24 | + * @var string |
| 25 | + */ |
| 26 | + private $deafult_error_prefix; |
| 27 | + |
| 28 | + /** |
| 29 | + * @var string |
| 30 | + */ |
| 31 | + private $deafult_error_content = 'UNKNOWN ERROR'; |
| 32 | + |
| 33 | + /** |
| 34 | + * SFWFilesDownloader constructor |
| 35 | + * |
| 36 | + * @param HTTPMultiRequestService|null $service Optional. Custom service instance for dependency injection. |
| 37 | + * @throws \InvalidArgumentException If service is not an instance of HTTPMultiRequestService |
| 38 | + */ |
| 39 | + public function __construct($service = null) |
| 40 | + { |
| 41 | + $this->deafult_error_prefix = basename(__CLASS__) . ': '; |
| 42 | + |
| 43 | + if ($service !== null && !$service instanceof HTTPMultiRequestService) { |
| 44 | + throw new \InvalidArgumentException( |
| 45 | + 'Service must be an instance of ' . HTTPMultiRequestService::class |
| 46 | + ); |
| 47 | + } |
| 48 | + |
| 49 | + $this->http_multi_request_service = $service ?: new HTTPMultiRequestService(); |
| 50 | + } |
| 51 | + |
| 52 | + /** |
| 53 | + * Downloads SFW data files from provided URLs with batch processing and retry logic |
| 54 | + * |
| 55 | + * Downloads files in batches to avoid server overload. Automatically retries failed downloads |
| 56 | + * with reduced batch size if necessary. Validates write permissions and URL format before processing. |
| 57 | + * |
| 58 | + * @param array|mixed $all_urls List of URLs to download files from |
| 59 | + * @param bool $direct_update Optional. If true, returns boolean result. If false, returns stage info array. |
| 60 | + * @param int sleep Pause in seconds before multi contracts run, default is 3 |
| 61 | + * |
| 62 | + * @return true|array True on success (direct update mode), or array with 'next_stage' key, |
| 63 | + * or array with 'error' key on failure, or array with 'update_args' for retry. |
| 64 | + */ |
| 65 | + public function downloadFiles($all_urls, $direct_update = false, $sleep = 3) |
| 66 | + { |
| 67 | + global $apbct; |
| 68 | + |
| 69 | + // Delay to prevent server overload |
| 70 | + sleep($sleep); |
| 71 | + |
| 72 | + // Validate write permissions for update folder |
| 73 | + if ( ! is_writable($apbct->fw_stats['updating_folder']) ) { |
| 74 | + return $this->responseStopUpdate('SFW UPDATE FOLDER IS NOT WRITABLE.'); |
| 75 | + } |
| 76 | + |
| 77 | + // Validate URLs parameter type |
| 78 | + if ( ! is_array($all_urls) ) { |
| 79 | + return $this->responseStopUpdate('URLS LIST SHOULD BE AN ARRAY'); |
| 80 | + } |
| 81 | + |
| 82 | + // Remove duplicates and reset array keys to sequential integers |
| 83 | + $all_urls = array_values(array_unique($all_urls)); |
| 84 | + |
| 85 | + // Get current batch size from settings or default |
| 86 | + $work_batch_size = SFWUpdateHelper::getSFWFilesBatchSize(); |
| 87 | + |
| 88 | + // Initialize batch processing variables |
| 89 | + $total_urls = count($all_urls); |
| 90 | + $batches = ceil($total_urls / $work_batch_size); |
| 91 | + $download_again = []; |
| 92 | + $written_urls = []; |
| 93 | + |
| 94 | + // Get or set default batch size for retry attempts |
| 95 | + $on_repeat_batch_size = !empty($apbct->data['sfw_update__batch_size']) |
| 96 | + ? TT::toInt($apbct->data['sfw_update__batch_size']) |
| 97 | + : 10; |
| 98 | + |
| 99 | + // Process URLs in batches |
| 100 | + for ($i = 0; $i < $batches; $i++) { |
| 101 | + // Extract current batch of URLs |
| 102 | + $current_batch_urls = array_slice($all_urls, $i * $work_batch_size, $work_batch_size); |
| 103 | + |
| 104 | + if (!empty($current_batch_urls)) { |
| 105 | + // Execute multi-request for current batch |
| 106 | + $multi_request_contract = $this->http_multi_request_service->setMultiContract($current_batch_urls); |
| 107 | + |
| 108 | + // Critical error: contract processing failed, stop update immediately |
| 109 | + if (!$multi_request_contract->process_done) { |
| 110 | + $error = !empty($multi_request_contract->error_msg) ? $multi_request_contract->error_msg : 'UNKNOWN ERROR'; |
| 111 | + return $this->responseStopUpdate($error); |
| 112 | + } |
| 113 | + |
| 114 | + // Handle failed downloads in this batch |
| 115 | + if (!empty($multi_request_contract->getFailedURLs())) { |
| 116 | + // Reduce batch size for retry if service suggests it |
| 117 | + if ($multi_request_contract->suggest_batch_reduce_to) { |
| 118 | + $on_repeat_batch_size = min($on_repeat_batch_size, $multi_request_contract->suggest_batch_reduce_to); |
| 119 | + } |
| 120 | + // Collect failed URLs for retry |
| 121 | + $download_again = array_merge($download_again, $multi_request_contract->getFailedURLs()); |
| 122 | + } |
| 123 | + |
| 124 | + // Write successfully downloaded content to files |
| 125 | + $write_result = $multi_request_contract->writeSuccessURLsContent($apbct->fw_stats['updating_folder']); |
| 126 | + |
| 127 | + // File write error occurred, stop update |
| 128 | + if (is_string($write_result)) { |
| 129 | + return $this->responseStopUpdate($write_result); |
| 130 | + } |
| 131 | + |
| 132 | + // Track successfully written URLs |
| 133 | + $written_urls = array_merge($written_urls, $write_result); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + // Some downloads failed, schedule retry with adjusted batch size |
| 138 | + if (!empty($download_again)) { |
| 139 | + $apbct->fw_stats['multi_request_batch_size'] = $on_repeat_batch_size; |
| 140 | + $apbct->save('data'); |
| 141 | + return $this->responseRepeatStage('FILES DOWNLOAD NOT COMPLETED, TRYING AGAIN', $download_again); |
| 142 | + } |
| 143 | + |
| 144 | + // Verify all URLs were successfully downloaded and written |
| 145 | + if (empty(array_diff($all_urls, $written_urls))) { |
| 146 | + return $this->responseSuccess($direct_update); |
| 147 | + } |
| 148 | + |
| 149 | + // Download incomplete with no retry - collect error information |
| 150 | + $last_contract_errors = isset($multi_request_contract) && $multi_request_contract->getContractsErrors() |
| 151 | + ? $multi_request_contract->getContractsErrors() |
| 152 | + : 'no known contract errors'; |
| 153 | + |
| 154 | + $error = 'FILES DOWNLOAD NOT COMPLETED - STOP UPDATE, ERRORS: ' . $last_contract_errors; |
| 155 | + return $this->responseStopUpdate($error); |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * Creates error response to stop the update process |
| 160 | + * |
| 161 | + * @param string $message Error message describing why update was stopped |
| 162 | + * |
| 163 | + * @return array Error response array with 'error' key |
| 164 | + */ |
| 165 | + private function responseStopUpdate($message): array |
| 166 | + { |
| 167 | + $message = is_string($message) ? $message : $this->deafult_error_content; |
| 168 | + $message = $this->deafult_error_prefix . $message; |
| 169 | + return [ |
| 170 | + 'error' => $message |
| 171 | + ]; |
| 172 | + } |
| 173 | + |
| 174 | + /** |
| 175 | + * Creates response to repeat current stage with modified arguments |
| 176 | + * |
| 177 | + * Used when downloads partially failed and should be retried with |
| 178 | + * potentially reduced batch size or different parameters. |
| 179 | + * |
| 180 | + * @param string $message Descriptive message about why stage needs repeating |
| 181 | + * @param array $args Arguments for retry attempt (typically failed URLs) |
| 182 | + * |
| 183 | + * @return array Response array with 'error' message and 'update_args' for retry |
| 184 | + */ |
| 185 | + private function responseRepeatStage($message, $args): array |
| 186 | + { |
| 187 | + $args = is_array($args) ? $args : []; |
| 188 | + $message = is_string($message) ? $message : $this->deafult_error_content; |
| 189 | + $message = $this->deafult_error_prefix . $message; |
| 190 | + return [ |
| 191 | + 'error' => $message, |
| 192 | + 'update_args' => [ |
| 193 | + 'args' => $args |
| 194 | + ], |
| 195 | + ]; |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * Creates success response to proceed to next stage or complete update |
| 200 | + * |
| 201 | + * @param bool $direct_update If true, returns simple boolean. If false, returns stage transition array. |
| 202 | + * |
| 203 | + * @return true|array True for direct update mode, or array with 'next_stage' key for staged updates |
| 204 | + */ |
| 205 | + private function responseSuccess($direct_update) |
| 206 | + { |
| 207 | + return $direct_update ? true : [ |
| 208 | + 'next_stage' => array( |
| 209 | + 'name' => 'apbct_sfw_update__create_tables' |
| 210 | + ) |
| 211 | + ]; |
| 212 | + } |
| 213 | +} |
0 commit comments