Issues (121)

src/Services/FileService.php (3 issues)

Labels
Severity
1
<?php
2
3
namespace Jidaikobo\Kontiki\Services;
4
5
/**
6
 * Class for handling file uploads and deletions.
7
 */
8
class FileService
9
{
10
    protected $uploadDir;
11
    protected $allowedTypes;
12
    protected $maxSize;
13
14
    /**
15
     * Constructor to initialize the upload directory and settings.
16
     *
17
     * @param string $uploadDir The directory where files will be uploaded.
18
     * @param array $allowedTypes An array of allowed MIME types.
19
     * @param int $maxSize The maximum allowed file size in bytes.
20
     */
21
    public function __construct(
22
        string $uploadDir,
23
        array $allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'],
24
        int $maxSize = 5000000
25
    ) {
26
        $this->uploadDir = $this->initializeUploadDir($uploadDir);
27
        $this->allowedTypes = $allowedTypes;
28
        $this->maxSize = $maxSize;
29
    }
30
31
    /**
32
     * Initialize the upload directory with year-based subdirectory.
33
     *
34
     * @param string $baseDir The base upload directory.
35
     *
36
     * @return string The initialized upload directory path.
37
     */
38
    protected function initializeUploadDir(string $baseDir): string
39
    {
40
        $uploadDir = rtrim($baseDir, '/') . '/' . date('Y') . '/';
41
        if (!is_dir($uploadDir)) {
42
            mkdir($uploadDir, 0755, true);
43
        }
44
        return $uploadDir;
45
    }
46
47
   /**
48
     * Handle the file upload.
49
     *
50
     * @param array $file The file array from $_FILES.
51
     *
52
     * @return array An array with 'success' (bool), 'path' (string), 'filename' (string), and 'errors' (array).
53
     */
54
    public function upload(array $file): array
55
    {
56
        $errors = $this->validateFile($file);
57
        if (!empty($errors)) {
58
            return $this->createErrorResponse($errors);
59
        }
60
61
        $sanitizedFileName = $this->sanitizeFileName($file['name']);
62
        $targetPath = $this->getUniqueFilePath($sanitizedFileName);
63
64
        if (move_uploaded_file($file['tmp_name'], $targetPath)) {
65
            return [
66
                'success' => true,
67
                'path' => $targetPath,
68
                'filename' => basename($targetPath),
69
                'errors' => [],
70
            ];
71
        }
72
73
        return $this->createErrorResponse(['Failed to move uploaded file.']);
74
    }
75
76
    /**
77
     * Validate the uploaded file.
78
     *
79
     * @param array $file The file array from $_FILES.
80
     * @return array An array of validation error messages.
81
     */
82
    protected function validateFile(array $file): array
83
    {
84
        $errors = [];
85
86
        // Validate MIME type
87
        $mimeType = mime_content_type($file['tmp_name']);
88
        if (!in_array($mimeType, $this->allowedTypes)) {
89
            $errors[] = "Invalid file type: $mimeType.";
90
        }
91
92
        // Validate file size
93
        if ($file['size'] > $this->maxSize) {
94
            $errors[] = "File exceeds maximum size of " . ($this->maxSize / 1000000) . " MB.";
95
        }
96
97
        return $errors;
98
    }
99
100
    /**
101
     * Sanitize the file name.
102
     *
103
     * @param string $fileName The original file name.
104
     * @return string The sanitized file name.
105
     */
106
    protected function sanitizeFileName(string $fileName): string
107
    {
108
        $originalName = pathinfo($fileName, PATHINFO_FILENAME);
109
        $extension = pathinfo($fileName, PATHINFO_EXTENSION);
110
        $asciiName = $this->convertToAscii($originalName);
0 ignored issues
show
It seems like $originalName can also be of type array; however, parameter $string of Jidaikobo\Kontiki\Servic...rvice::convertToAscii() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
        $asciiName = $this->convertToAscii(/** @scrutinizer ignore-type */ $originalName);
Loading history...
111
        return $asciiName . ($extension ? ".$extension" : '');
112
    }
113
114
    /**
115
     * Get a unique file path by appending a numeric suffix if necessary.
116
     *
117
     * @param string $fileName The sanitized file name.
118
     * @return string The unique file path.
119
     */
120
    protected function getUniqueFilePath(string $fileName): string
121
    {
122
        $targetPath = $this->uploadDir . $fileName;
123
        $suffix = 1;
124
125
        while (file_exists($targetPath)) {
126
            $targetPath = $this->uploadDir . pathinfo($fileName, PATHINFO_FILENAME) . "_$suffix." . pathinfo($fileName, PATHINFO_EXTENSION);
0 ignored issues
show
Are you sure pathinfo($fileName, Jida...ces\PATHINFO_EXTENSION) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

126
            $targetPath = $this->uploadDir . pathinfo($fileName, PATHINFO_FILENAME) . "_$suffix." . /** @scrutinizer ignore-type */ pathinfo($fileName, PATHINFO_EXTENSION);
Loading history...
Are you sure pathinfo($fileName, Jida...ices\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

126
            $targetPath = $this->uploadDir . /** @scrutinizer ignore-type */ pathinfo($fileName, PATHINFO_FILENAME) . "_$suffix." . pathinfo($fileName, PATHINFO_EXTENSION);
Loading history...
127
            $suffix++;
128
        }
129
130
        return $targetPath;
131
    }
132
133
    /**
134
     * Convert a string to ASCII, replacing non-ASCII characters with underscores.
135
     *
136
     * @param string $string The input string.
137
     * @return string The ASCII converted string.
138
     */
139
    protected function convertToAscii(string $string): string
140
    {
141
        $ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $string);
142
        return preg_replace('/[^a-zA-Z0-9]+/', '_', $ascii);
143
    }
144
145
    /**
146
     * Create an error response.
147
     *
148
     * @param array $errors The list of error messages.
149
     * @return array The error response array.
150
     */
151
    protected function createErrorResponse(array $errors): array
152
    {
153
        return [
154
            'success' => false,
155
            'path' => '',
156
            'filename' => '',
157
            'errors' => $errors,
158
        ];
159
    }
160
161
    /**
162
     * Delete a file from the upload directory.
163
     *
164
     * @param string $filePath The relative path of the file to delete.
165
     * @return bool True on success, false on failure.
166
     */
167
    public function delete(string $filePath): bool
168
    {
169
        $fullPath = $this->uploadDir . basename($filePath);
170
        return file_exists($fullPath) && unlink($fullPath);
171
    }
172
}
173