| Total Complexity | 92 |
| Total Lines | 601 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 0 |
Complex classes like TemporaryAttachment often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use TemporaryAttachment, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 26 | class TemporaryAttachment extends ValuesContainer |
||
| 27 | { |
||
| 28 | /** |
||
| 29 | * {@inheritDoc} |
||
| 30 | */ |
||
| 31 | public function __construct($data = null) |
||
| 37 | } |
||
| 38 | |||
| 39 | /** |
||
| 40 | * Deletes a temporary attachment from the filesystem |
||
| 41 | * |
||
| 42 | * @param bool $fatal |
||
| 43 | * @return bool |
||
| 44 | * @throws ElkException thrown if fatal is true |
||
| 45 | */ |
||
| 46 | public function remove(bool $fatal = true): bool |
||
| 57 | } |
||
| 58 | |||
| 59 | /** |
||
| 60 | * Checks if the file (not a directory) exists, and is editable, in the file system. |
||
| 61 | */ |
||
| 62 | public function fileWritable(): bool |
||
| 63 | { |
||
| 64 | $path = $this->data['tmp_name'] ?? ''; |
||
| 65 | if ($path === '') |
||
| 66 | { |
||
| 67 | return false; |
||
| 68 | } |
||
| 69 | |||
| 70 | $fs = FileFunctions::instance(); |
||
| 71 | return $fs->fileExists($path) && $fs->isWritable($path); |
||
| 72 | } |
||
| 73 | |||
| 74 | /** |
||
| 75 | * Returns an array of names of temporary attachments. |
||
| 76 | * |
||
| 77 | * @return string |
||
| 78 | */ |
||
| 79 | public function getName(): string |
||
| 80 | { |
||
| 81 | return $this->data['name']; |
||
| 82 | } |
||
| 83 | |||
| 84 | /** |
||
| 85 | * Error setter, adds errors to the stack |
||
| 86 | * |
||
| 87 | * @param $error |
||
| 88 | */ |
||
| 89 | public function setErrors($error): void |
||
| 90 | { |
||
| 91 | // Normalize input to a flat list of error items where each item is an array: |
||
| 92 | // [code] or [code, [args]] |
||
| 93 | if ($error === null || $error === '') |
||
| 94 | { |
||
| 95 | return; |
||
| 96 | } |
||
| 97 | |||
| 98 | $append = function ($item) { |
||
| 99 | if ($item === null || $item === '') |
||
| 100 | { |
||
| 101 | return; |
||
| 102 | } |
||
| 103 | |||
| 104 | // If a string, wrap as [code] |
||
| 105 | if (!is_array($item)) |
||
| 106 | { |
||
| 107 | $this->data['errors'][] = [$item]; |
||
| 108 | return; |
||
| 109 | } |
||
| 110 | |||
| 111 | // If it looks like a single error tuple [code, [args]] or [code] |
||
| 112 | // keep as-is; otherwise, best-effort wrap |
||
| 113 | if (isset($item[0]) && (is_string($item[0]) || is_scalar($item[0]))) |
||
| 114 | { |
||
| 115 | // If args present but not an array, wrap it |
||
| 116 | if (isset($item[1]) && !is_array($item[1])) |
||
| 117 | { |
||
| 118 | $item[1] = [$item[1]]; |
||
| 119 | } |
||
| 120 | |||
| 121 | $this->data['errors'][] = $item; |
||
| 122 | return; |
||
| 123 | } |
||
| 124 | |||
| 125 | // Fallback: wrap whole structure as a single error payload |
||
| 126 | $this->data['errors'][] = [$item]; |
||
| 127 | }; |
||
| 128 | |||
| 129 | // If we received a list of errors (mixed strings and arrays), append each |
||
| 130 | if (is_array($error) && !(isset($error[0]) && is_string($error[0]) && (count($error) === 1 || (count($error) === 2 && isset($error[1]) && is_array($error[1]))))) |
||
| 131 | { |
||
| 132 | foreach ($error as $e) |
||
| 133 | { |
||
| 134 | $append($e); |
||
| 135 | } |
||
| 136 | } |
||
| 137 | else |
||
| 138 | { |
||
| 139 | $append($error); |
||
| 140 | } |
||
| 141 | } |
||
| 142 | |||
| 143 | /** |
||
| 144 | * Return if errors were found for this attachment attempt |
||
| 145 | * |
||
| 146 | * @return bool |
||
| 147 | */ |
||
| 148 | public function hasErrors(): bool |
||
| 149 | { |
||
| 150 | return !empty($this->data['errors']); |
||
| 151 | } |
||
| 152 | |||
| 153 | /** |
||
| 154 | * Error getter |
||
| 155 | * |
||
| 156 | * @return array |
||
| 157 | */ |
||
| 158 | public function getErrors(): mixed |
||
| 159 | { |
||
| 160 | return $this->data['errors']; |
||
| 161 | } |
||
| 162 | |||
| 163 | /** |
||
| 164 | * Return the attachment filesize |
||
| 165 | * |
||
| 166 | * @return int |
||
| 167 | */ |
||
| 168 | public function getSize(): int |
||
| 169 | { |
||
| 170 | return $this->data['size']; |
||
| 171 | } |
||
| 172 | |||
| 173 | /** |
||
| 174 | * Return the mime type of the file, if available |
||
| 175 | * |
||
| 176 | * @return string |
||
| 177 | */ |
||
| 178 | public function getMime(): string |
||
| 179 | { |
||
| 180 | return $this->data['mime'] ?? ''; |
||
| 181 | } |
||
| 182 | |||
| 183 | /** |
||
| 184 | * Checks if the file exists, and is editable, in the file system. |
||
| 185 | */ |
||
| 186 | public function fileExists(): bool |
||
| 187 | { |
||
| 188 | $path = $this->data['tmp_name'] ?? ''; |
||
| 189 | return $path !== '' && FileFunctions::instance()->fileExists($path); |
||
| 190 | } |
||
| 191 | |||
| 192 | /** |
||
| 193 | * Renaming and moving |
||
| 194 | * |
||
| 195 | * @param $file_path |
||
| 196 | */ |
||
| 197 | public function moveTo($file_path): void |
||
| 198 | { |
||
| 199 | $destination = $file_path . '/' . $this->data['attachid']; |
||
| 200 | rename($this->data['tmp_name'], $destination); |
||
| 201 | $this->data['tmp_name'] = $destination; |
||
| 202 | } |
||
| 203 | |||
| 204 | /** |
||
| 205 | * Moves the uploaded file to a specified destination folder. |
||
| 206 | * |
||
| 207 | * @param string $file_path The destination folder path. |
||
| 208 | * @param bool $strict Determines whether to use strict file moving or not. |
||
| 209 | * @return bool Returns true if the file is moved successfully, false otherwise. |
||
| 210 | */ |
||
| 211 | public function moveUploaded(string $file_path, bool $strict = true): bool |
||
| 212 | { |
||
| 213 | $destName = $file_path . '/' . $this->data['attachid']; |
||
| 214 | |||
| 215 | if (!$strict) |
||
| 216 | { |
||
| 217 | $result = rename($this->data['tmp_name'], $destName); |
||
| 218 | } |
||
| 219 | else |
||
| 220 | { |
||
| 221 | // Move the file to the attachment folder with a temp name for now. |
||
| 222 | set_error_handler(static function () { /* ignore warnings */ }); |
||
| 223 | try |
||
| 224 | { |
||
| 225 | $result = move_uploaded_file($this->data['tmp_name'], $destName); |
||
| 226 | } |
||
| 227 | catch (\Throwable) |
||
| 228 | { |
||
| 229 | $result = false; |
||
| 230 | } |
||
| 231 | finally |
||
| 232 | { |
||
| 233 | restore_error_handler(); |
||
| 234 | } |
||
| 235 | } |
||
| 236 | |||
| 237 | if ($result === true) |
||
| 238 | { |
||
| 239 | $this->data['tmp_name'] = $destName; |
||
| 240 | FileFunctions::instance()->chmod($destName); |
||
| 241 | |||
| 242 | return true; |
||
| 243 | } |
||
| 244 | |||
| 245 | $this->setErrors('attach_timeout'); |
||
| 246 | $this->unlinkFile(); |
||
| 247 | |||
| 248 | return false; |
||
| 249 | } |
||
| 250 | |||
| 251 | /** |
||
| 252 | * Sets the folder ID value in the object's data. |
||
| 253 | * |
||
| 254 | * @param int $id The ID to be assigned to the folder. |
||
| 255 | * @return void |
||
| 256 | */ |
||
| 257 | public function setIdFolder(int $id): void |
||
| 258 | { |
||
| 259 | $this->data['id_folder'] = $id; |
||
| 260 | } |
||
| 261 | |||
| 262 | /** |
||
| 263 | * Performs various checks on an uploaded file. |
||
| 264 | * |
||
| 265 | * @param AttachmentsDirectory $attachmentDirectory |
||
| 266 | * @return bool |
||
| 267 | * @throws ElkException attach_check_nag |
||
| 268 | */ |
||
| 269 | public function doElkarteUploadChecks(AttachmentsDirectory $attachmentDirectory): bool |
||
| 270 | { |
||
| 271 | global $context; |
||
| 272 | |||
| 273 | // If there were already errors at this point, no need to check further |
||
| 274 | if (!empty($this->data['errors'])) |
||
| 275 | { |
||
| 276 | return false; |
||
| 277 | } |
||
| 278 | |||
| 279 | // Apply some additional checks |
||
| 280 | if (empty($this->data['attachid'])) |
||
| 281 | { |
||
| 282 | $error = 'attachid'; |
||
| 283 | } |
||
| 284 | // @TODO this needs to go away, not sure where though. |
||
| 285 | elseif (empty($context['attachments'])) |
||
| 286 | { |
||
| 287 | $error = '$context[\'attachments\']'; |
||
| 288 | } |
||
| 289 | |||
| 290 | // Let's get their attention. |
||
| 291 | if (!empty($error)) |
||
| 292 | { |
||
| 293 | throw new ElkException('attach_check_nag', 'debug', [$error]); |
||
| 294 | } |
||
| 295 | |||
| 296 | // Just in case this slipped by the first checks, we stop it here and now |
||
| 297 | if ($this->data['size'] === 0) |
||
| 298 | { |
||
| 299 | $this->setErrors('attach_0_byte_file'); |
||
| 300 | |||
| 301 | return false; |
||
| 302 | } |
||
| 303 | |||
| 304 | // Allow addons to make their own pre checks / adjustments |
||
| 305 | call_integration_hook('integrate_attachment_checks', [$this->data['attachid']]); |
||
| 306 | |||
| 307 | // Did you pack this bag yourself? |
||
| 308 | $this->checkImageContents(); |
||
| 309 | |||
| 310 | // WebP may require special processing that will affect size/type |
||
| 311 | $this->convertFromWebp(); |
||
| 312 | |||
| 313 | // We may allow resizing uploaded images, so they take less room |
||
| 314 | $this->adjustImageSizeType(); |
||
| 315 | |||
| 316 | // We may want to correct rotated images |
||
| 317 | $this->autoRotate(); |
||
| 318 | |||
| 319 | // Run our batch of tests, set any errors along the way |
||
| 320 | $this->checkDirectorySpace($attachmentDirectory); |
||
| 321 | $this->checkFileSize(); |
||
| 322 | $this->checkTotalUploadSize(); |
||
| 323 | $this->checkTotalUploadCount(); |
||
| 324 | $this->checkFileExtensions(); |
||
| 325 | |||
| 326 | // Undo the math if there's an error |
||
| 327 | if ($this->hasErrors()) |
||
| 328 | { |
||
| 329 | if (isset($context['dir_size'])) |
||
| 330 | { |
||
| 331 | $context['dir_size'] -= $this->data['size']; |
||
| 332 | } |
||
| 333 | |||
| 334 | if (isset($context['dir_files'])) |
||
| 335 | { |
||
| 336 | $context['dir_files']--; |
||
| 337 | } |
||
| 338 | |||
| 339 | $context['attachments']['total_size'] -= $this->data['size']; |
||
| 340 | $context['attachments']['quantity']--; |
||
| 341 | |||
| 342 | return false; |
||
| 343 | } |
||
| 344 | |||
| 345 | return true; |
||
| 346 | } |
||
| 347 | |||
| 348 | /** |
||
| 349 | * If we have a valid image type, inspect to see if there is any |
||
| 350 | * injected code fragments. If found re encode to remove those fragments |
||
| 351 | */ |
||
| 352 | public function checkImageContents(): void |
||
| 353 | { |
||
| 354 | global $modSettings; |
||
| 355 | |||
| 356 | // First, the dreaded security check. Sorry folks, but this should't be avoided |
||
| 357 | $image = new Image($this->data['tmp_name']); |
||
| 358 | if ($image->isImageLoaded()) |
||
| 359 | { |
||
| 360 | $this->data['imagesize'] = $image->getImageDimensions(); |
||
| 361 | $this->data['size'] = $image->getFilesize(); |
||
| 362 | try |
||
| 363 | { |
||
| 364 | if (!$image->checkImageContents()) |
||
| 365 | { |
||
| 366 | // It's bad. Last chance, maybe we can re-encode it? |
||
| 367 | if (empty($modSettings['attachment_image_reencode']) || (!$image->reEncodeImage())) |
||
| 368 | { |
||
| 369 | // Nothing to do: not allowed or not successful re-encoding it. |
||
| 370 | $this->setErrors('bad_attachment'); |
||
| 371 | $this->data['imagesize'] = []; |
||
| 372 | } |
||
| 373 | else |
||
| 374 | { |
||
| 375 | $this->data['size'] = $image->getFilesize(); |
||
| 376 | } |
||
| 377 | } |
||
| 378 | } |
||
| 379 | catch (\Exception) |
||
| 380 | { |
||
| 381 | $this->setErrors('bad_attachment'); |
||
| 382 | } |
||
| 383 | } |
||
| 384 | |||
| 385 | unset($image); |
||
| 386 | } |
||
| 387 | |||
| 388 | /** |
||
| 389 | * If enabled, call the attachment image resizing functions. These reduce the image WxH |
||
| 390 | * and potentially change the format in order to reduce size. |
||
| 391 | */ |
||
| 392 | public function adjustImageSizeType(): void |
||
| 393 | { |
||
| 394 | global $modSettings; |
||
| 395 | |||
| 396 | // Auto resize enabled, then do sizing manipulations up front |
||
| 397 | if (!empty($modSettings['attachmentSizeLimit']) && !empty($modSettings['attachment_image_resize_enabled'])) |
||
| 398 | { |
||
| 399 | $autoSizer = new ImageUploadResize(); |
||
| 400 | $autoSizer->autoResize($this->data); |
||
| 401 | } |
||
| 402 | } |
||
| 403 | |||
| 404 | /** |
||
| 405 | * If the admin does not want to save webP (attachment_webp_enable is off) but they accept |
||
| 406 | * webp extensions and the server has webp capabilities, then webP -> PNG or -> JPG (best choice) |
||
| 407 | * based on the input image |
||
| 408 | * |
||
| 409 | * @return void |
||
| 410 | */ |
||
| 411 | public function convertFromWebp(): void |
||
| 412 | { |
||
| 413 | global $modSettings; |
||
| 414 | |||
| 415 | // We may have to adjust for webp based on ACP settings |
||
| 416 | if (empty($this->data['imagesize'][2]) |
||
| 417 | || $this->data['imagesize'][2] !== IMAGETYPE_WEBP |
||
| 418 | || !empty($modSettings['attachment_webp_enable']) |
||
| 419 | || (!empty($modSettings['attachmentCheckExtensions']) && stripos($modSettings['attachmentExtensions'], ',webp') === false)) |
||
| 420 | { |
||
| 421 | return; |
||
| 422 | } |
||
| 423 | |||
| 424 | // Is a webp image and manipulation is possible? |
||
| 425 | $image = new Image($this->data['tmp_name']); |
||
| 426 | if ($image->hasWebpSupport()) |
||
| 427 | { |
||
| 428 | $format = $image->getDefaultFormat(); |
||
| 429 | if ($image->isImageLoaded() && $image->saveImage($this->data['tmp_name'], $format)) |
||
| 430 | { |
||
| 431 | $valid_mime = getValidMimeImageType($format); |
||
| 432 | $ext = str_replace('jpeg', 'jpg', substr($valid_mime, strpos($valid_mime, '/') + 1)); |
||
| 433 | |||
| 434 | // Update to what it now is (webp to png or jpg) |
||
| 435 | $update = [ |
||
| 436 | 'size' => $image->getFilesize(), |
||
| 437 | 'imagesize' => $image->getImageDimensions(), |
||
| 438 | 'type' => $valid_mime, |
||
| 439 | 'mime' => $valid_mime, |
||
| 440 | 'name' => $this->data['name'] . '.' . $ext |
||
| 441 | ]; |
||
| 442 | |||
| 443 | $this->data = array_merge($this->data, $update); |
||
| 444 | } |
||
| 445 | } |
||
| 446 | } |
||
| 447 | |||
| 448 | /** |
||
| 449 | * Is there room in the directory for this file |
||
| 450 | * |
||
| 451 | * @param AttachmentsDirectory $attachmentDirectory |
||
| 452 | */ |
||
| 453 | public function checkDirectorySpace(AttachmentsDirectory $attachmentDirectory): void |
||
| 454 | { |
||
| 455 | try |
||
| 456 | { |
||
| 457 | $attachmentDirectory->checkDirSpace($this); |
||
| 458 | } |
||
| 459 | catch (\Exception $exception) |
||
| 460 | { |
||
| 461 | $this->setErrors($exception->getMessage()); |
||
| 462 | } |
||
| 463 | } |
||
| 464 | |||
| 465 | /** |
||
| 466 | * Is the file larger than we accept |
||
| 467 | */ |
||
| 468 | public function checkFileSize(): void |
||
| 469 | { |
||
| 470 | global $modSettings; |
||
| 471 | |||
| 472 | // Is the file too big? |
||
| 473 | if (empty($modSettings['attachmentSizeLimit'])) |
||
| 474 | { |
||
| 475 | return; |
||
| 476 | } |
||
| 477 | |||
| 478 | if ($this->data['size'] <= $modSettings['attachmentSizeLimit'] * 1024) |
||
| 479 | { |
||
| 480 | return; |
||
| 481 | } |
||
| 482 | |||
| 483 | $this->setErrors([ |
||
| 484 | 'file_too_big', [ |
||
| 485 | comma_format($modSettings['attachmentSizeLimit'], 0) |
||
| 486 | ] |
||
| 487 | ]); |
||
| 488 | } |
||
| 489 | |||
| 490 | /** |
||
| 491 | * Check if they are sending too much data in a single post |
||
| 492 | */ |
||
| 493 | public function checkTotalUploadSize(): void |
||
| 494 | { |
||
| 495 | global $context, $modSettings; |
||
| 496 | |||
| 497 | // Check the total upload size for this post... |
||
| 498 | $context['attachments']['total_size'] += $this->data['size']; |
||
| 499 | if (empty($modSettings['attachmentPostLimit'])) |
||
| 500 | { |
||
| 501 | return; |
||
| 502 | } |
||
| 503 | |||
| 504 | if ($context['attachments']['total_size'] <= $modSettings['attachmentPostLimit'] * 1024) |
||
| 505 | { |
||
| 506 | return; |
||
| 507 | } |
||
| 508 | |||
| 509 | $this->setErrors([ |
||
| 510 | 'attach_max_total_file_size', [ |
||
| 511 | comma_format($modSettings['attachmentPostLimit'], 0), |
||
| 512 | comma_format($modSettings['attachmentPostLimit'] - (($context['attachments']['total_size'] - $this->data['size']) / 1024), 0) |
||
| 513 | ] |
||
| 514 | ]); |
||
| 515 | } |
||
| 516 | |||
| 517 | /** |
||
| 518 | * Check if they are sending too many files at once |
||
| 519 | */ |
||
| 520 | public function checkTotalUploadCount(): void |
||
| 546 | ] |
||
| 547 | ]); |
||
| 548 | } |
||
| 549 | |||
| 550 | /** |
||
| 551 | * If enabled, check if this is a filetype we accept (by extension) |
||
| 552 | */ |
||
| 553 | public function checkFileExtensions(): void |
||
| 554 | { |
||
| 555 | global $modSettings; |
||
| 556 | |||
| 557 | // File extension check |
||
| 558 | if (!empty($modSettings['attachmentCheckExtensions'])) |
||
| 559 | { |
||
| 560 | $allowed = explode(',', strtolower($modSettings['attachmentExtensions'])); |
||
| 561 | $allowed = array_map('trim', $allowed); |
||
| 562 | |||
| 563 | if (!in_array(strtolower(substr(strrchr($this->data['name'], '.'), 1)), $allowed, true)) |
||
| 564 | { |
||
| 565 | $allowed_extensions = strtr(strtolower($modSettings['attachmentExtensions']), [',' => ', ']); |
||
| 566 | $this->setErrors([ |
||
| 567 | 'cant_upload_type', [ |
||
| 568 | $allowed_extensions |
||
| 569 | ] |
||
| 570 | ]); |
||
| 571 | } |
||
| 572 | } |
||
| 573 | } |
||
| 574 | |||
| 575 | /** |
||
| 576 | * Rotate an image top side up based on its EXIF data |
||
| 577 | */ |
||
| 578 | public function autoRotate(): void |
||
| 591 | } |
||
| 592 | } |
||
| 593 | } |
||
| 594 | |||
| 595 | /** |
||
| 596 | * Checks if a file existence/permission and if granted will attempt |
||
| 597 | * to remove/unlink the file. |
||
| 598 | * |
||
| 599 | * @return bool |
||
| 600 | */ |
||
| 601 | private function unlinkFile(): bool |
||
| 627 | } |
||
| 628 | } |
||
| 629 |