CRUDTrait::handleFileUpload()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 5
eloc 19
nc 5
nop 2
dl 0
loc 35
rs 9.3222
c 1
b 0
f 1
1
<?php
2
3
namespace Jidaikobo\Kontiki\Controllers\FileControllerTraits;
4
5
use Jidaikobo\Log;
6
use Jidaikobo\Kontiki\Utils\MessageUtils;
7
use Psr\Http\Message\ResponseInterface as Response;
8
use Psr\Http\Message\ServerRequestInterface as Request;
9
10
trait CRUDTrait
11
{
12
    public function getCsrfToken(Request $request, Response $response): Response
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

12
    public function getCsrfToken(/** @scrutinizer ignore-unused */ Request $request, Response $response): Response

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
13
    {
14
        $data = ['csrf_token' => $this->csrfManager->getToken()];
15
        return $this->jsonResponse($response, $data);
0 ignored issues
show
Bug introduced by
It seems like jsonResponse() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

15
        return $this->/** @scrutinizer ignore-call */ jsonResponse($response, $data);
Loading history...
16
    }
17
18
    /**
19
     * Handles file upload via an AJAX request.
20
     * This method processes the uploaded file, moves it to the specified directory,
21
     * and returns a JSON response indicating the result of the operation.
22
     *
23
     * @return Response
24
     */
25
    public function handleFileUpload(Request $request, Response $response): Response
26
    {
27
        $parsedBody = $request->getParsedBody() ?? [];
28
29
        // CSRF Token validation
30
        $errorResponse = $this->validateCsrfForJson($parsedBody, $response);
0 ignored issues
show
Bug introduced by
It seems like validateCsrfForJson() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

30
        /** @scrutinizer ignore-call */ 
31
        $errorResponse = $this->validateCsrfForJson($parsedBody, $response);
Loading history...
31
        if ($errorResponse) {
32
            return $errorResponse;
33
        }
34
35
        // prepare file
36
        $uploadedFile = $this->prepareUploadedFile($request);
37
        if (!$uploadedFile) {
38
            return $this->errorResponse($response, $this->getMessages()['file_missing'], 400);
0 ignored issues
show
Bug introduced by
It seems like getMessages() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

38
            return $this->errorResponse($response, $this->/** @scrutinizer ignore-call */ getMessages()['file_missing'], 400);
Loading history...
Bug introduced by
It seems like errorResponse() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

38
            return $this->/** @scrutinizer ignore-call */ errorResponse($response, $this->getMessages()['file_missing'], 400);
Loading history...
39
        }
40
41
        // upload file
42
        $uploadResult = $this->fileService->upload($uploadedFile);
43
        if (!$uploadResult['success']) {
44
            return $this->errorResponse($response, $this->getMessages()['upload_error'], 500);
45
        }
46
47
        // update database
48
        $fileUrl = $this->pathToUrl($uploadResult['path']);
0 ignored issues
show
Bug introduced by
It seems like pathToUrl() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

48
        /** @scrutinizer ignore-call */ 
49
        $fileUrl = $this->pathToUrl($uploadResult['path']);
Loading history...
49
        $fileData = ['path' => $fileUrl];
50
        $validationResult = $this->validateAndSave($fileData, $response);
51
        if ($validationResult instanceof Response) {
52
            return $validationResult; // return error response
53
        }
54
        $data = $validationResult; // this is the inserted data
55
56
        // success
57
        return $this->jsonResponse($response, [
58
                'message' => $this->getMessages()['upload_success'],
59
                'data' => $data
60
            ]);
61
    }
62
63
    protected function prepareUploadedFile(Request $request): ?array
64
    {
65
        $uploadedFiles = $request->getUploadedFiles();
66
        $uploadedFile = $uploadedFiles['attachment'] ?? null;
67
68
        if ($uploadedFile && $uploadedFile->getError() === UPLOAD_ERR_OK) {
69
            return [
70
                'name' => $uploadedFile->getClientFilename(),
71
                'type' => $uploadedFile->getClientMediaType(),
72
                'tmp_name' => $uploadedFile->getStream()->getMetadata('uri'),
73
                'size' => $uploadedFile->getSize(),
74
            ];
75
        }
76
77
        return null;
78
    }
79
80
    private function validateAndSave(array $fileData, Response $response): Response|array|null
81
    {
82
        $validationResult = $this->model->validate(
83
            $fileData,
84
            ['context' => 'create']
85
        );
86
87
        if (!$validationResult['valid']) {
88
            return $this->messageResponse(
0 ignored issues
show
Bug introduced by
It seems like messageResponse() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

88
            return $this->/** @scrutinizer ignore-call */ messageResponse(
Loading history...
89
                $response,
90
                MessageUtils::errorHtml($validationResult['errors'], $this->model),
91
                405
92
            );
93
        }
94
95
        $insertId = $this->model->create($fileData);
96
        if (!$insertId) {
97
            return $this->errorResponse(
98
                $response,
99
                $this->getMessages()['database_update_failed'],
100
                500
101
            );
102
        }
103
104
        $data = $this->model->getById($insertId);
105
106
        return $data;
107
    }
108
109
    /**
110
     * Handles the AJAX request to update a file's data in the database.
111
     * Validates the CSRF token, retrieves the file details by ID,
112
     * updates the file information, and returns a JSON response indicating success or failure.
113
     *
114
     * @return Response
115
     */
116
    public function handleUpdate(Request $request, Response $response): Response
117
    {
118
        $parsedBody = $request->getParsedBody() ?? [];
119
120
        // CSRF Token validation
121
        $errorResponse = $this->validateCsrfForJson($parsedBody, $response);
122
        if ($errorResponse) {
123
            return $errorResponse;
124
        }
125
126
        // Get the file ID from the POST request
127
        $fileId = $parsedBody['id'] ?? 0; // Default to 0 if no ID is provided
128
129
        // Retrieve the file details from the database using the file ID
130
        $data = $this->model->getById($fileId);
131
132
        if (!$data) {
133
            $message = $this->getMessages()['file_not_found'];
134
            return $this->messageResponse($response, $message, 405);
135
        }
136
137
        // Update the description field
138
        $data['description'] = $parsedBody['description'] ?? $data['description'];
139
140
        // Update the main item
141
        $result = $this->update($data, $fileId);
142
143
        if ($result['success']) {
144
            $message = $this->getMessages()['update_success'];
145
            return $this->messageResponse($response, $message, 200);
146
        } else {
147
            $message = MessageUtils::errorHtml($result['errors'], $this->model);
148
            return $this->messageResponse($response, $message, 405);
149
        }
150
    }
151
152
    /**
153
     * Validate and process data for create or edit actions.
154
     *
155
     * @param array $data The input data.
156
     * @param int|null $id The ID of the item to update (required for edit).
157
     * @return array The result containing 'success' (bool) and 'errors' (array).
158
     */
159
    protected function update(array $data, int $id = null)
160
    {
161
        $results = $this->model->validate(
162
            $data,
163
            ['id' => $id, 'context' => 'edit']
164
        );
165
166
        if ($results['valid'] !== true) {
167
            if (isset($results['errors']['description'])) {
168
                $newKey = 'eachDescription_' . $id;
169
                $results['errors']['description']['htmlName'] = $newKey;
170
            }
171
172
            return [
173
                'success' => false,
174
                'errors' => $results['errors']
175
            ];
176
        }
177
178
        // Process if valid
179
        $success = $this->model->update($id, $data);
180
181
        return [
182
            'success' => $success,
183
            'errors' => $success ? [] : ["Failed to update item."]
184
        ];
185
    }
186
187
    /**
188
     * Handles the deletion of a file via AJAX.
189
     *
190
     * This method validates the CSRF token, checks the POST request for the file ID,
191
     * retrieves the file from the database, deletes the corresponding file from the server,
192
     * and updates the database to remove the file record.
193
     * If any of these steps fail, an appropriate error message is returned as a JSON response.
194
     *
195
     * @return Response
196
     */
197
    public function handleDelete(Request $request, Response $response): Response
198
    {
199
        $parsedBody = $request->getParsedBody() ?? [];
200
201
        // CSRF Token validation
202
        $errorResponse = $this->validateCsrfForJson($parsedBody, $response);
203
        if ($errorResponse) {
204
            return $errorResponse;
205
        }
206
207
        // Get the file ID from the POST request
208
        $fileId = $parsedBody['id'] ?? 0; // Default to 0 if no ID is provided
209
210
        // Retrieve the file details from the database using the file ID
211
        $data = $this->model->getById($fileId);
212
213
        if (!$data) {
214
            $message = $this->getMessages()['file_not_found'];
215
            return $this->messageResponse($response, $message, 405);
216
        }
217
218
        // Delete the file from the server
219
        $filePath = $data['path'];
220
221
        if ($this->deleteFileFromSystem($filePath)) {
222
            Log::write("File deleted: " . $filePath);
223
        } else {
224
            $message = $this->getMessages()['file_delete_failed'];
225
            return $this->messageResponse($response, $message, 500);
226
        }
227
228
        // Remove the file record from the database
229
        $deleteSuccess = $this->model->delete($fileId);
230
        if (!$deleteSuccess) {
231
            $message = $this->getMessages()['db_update_failed'];
232
            return $this->messageResponse($response, $message, 500);
233
        }
234
235
        // Send a success response back
236
        $message = $this->getMessages()['file_delete_success'];
237
        return $this->messageResponse($response, $message, 200);
238
    }
239
240
    private function deleteFileFromSystem(string $filePath): bool
241
    {
242
        if (file_exists($filePath)) {
243
            return unlink($filePath);
244
        }
245
        return false;
246
    }
247
}
248