• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visavi / rotor / 27440345661

12 Jun 2026 08:11PM UTC coverage: 16.687% (+0.04%) from 16.644%
27440345661

push

github

visavi
Упростил Registry модулей, поправил метки в жалобах, улучшил подсветку комментария при переходе по якорю, улучшил отправку жалобы

6 of 41 new or added lines in 7 files covered. (14.63%)

2 existing lines in 2 files now uncovered.

960 of 5753 relevant lines covered (16.69%)

2.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/app/Http/Controllers/AjaxController.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Http\Controllers;
6

7
use App\Classes\Registry;
8
use App\Classes\Validator;
9
use App\Models\Comment;
10
use App\Models\File;
11
use App\Models\Message;
12
use App\Models\Poll;
13
use App\Models\Spam;
14
use App\Models\Sticker;
15
use Illuminate\Database\Eloquent\Model;
16
use Illuminate\Database\Eloquent\Relations\MorphOne;
17
use Illuminate\Database\Eloquent\Relations\Relation;
18
use Illuminate\Http\JsonResponse;
19
use Illuminate\Http\Request;
20
use Illuminate\Support\Facades\DB;
21
use Illuminate\Support\Str;
22

23
class AjaxController extends Controller
24
{
25
    /**
26
     * Отправляет жалобу на сообщение
27
     */
28
    public function complaint(Request $request, Validator $validator): JsonResponse
×
29
    {
30
        $path = null;
×
31
        $model = false;
×
32
        $id = int($request->input('id'));
×
33
        $type = $request->input('type');
×
34
        $page = $request->input('page');
×
35

36
        switch ($type) {
37
            case Message::$morphName:
×
38
                $model = Message::query()->find($id);
×
39
                break;
×
40

NEW
41
            case Comment::$morphName:
×
42
                $model = Comment::query()->find($id);
×
43
                $path = $model?->getViewUrl(false);
×
44
                break;
×
45

46
            default:
47
                if (isset(Registry::$complaintTypes[$type])) {
×
48
                    $result = (Registry::$complaintTypes[$type])($id, $page);
×
49
                    $model = $result['model'] ?? null;
×
50
                    $path = $result['path'] ?? null;
×
51
                }
52
                break;
×
53
        }
54

55
        $spam = Spam::query()->where(['relate_type' => $type, 'relate_id' => $id])->first();
×
56

57
        $validator
×
58
            ->true($model, __('main.message_not_found'))
×
59
            ->false($spam, __('ajax.complaint_already_sent'));
×
60

61
        if ($validator->isValid()) {
×
62
            Spam::query()->create([
×
63
                'relate_type' => $type,
×
64
                'relate_id'   => $model->id,
×
65
                'user_id'     => getUser('id'),
×
66
                'path'        => $path,
×
67
                'created_at'  => SITETIME,
×
68
            ]);
×
69

70
            return response()->json(['success' => true]);
×
71
        }
72

73
        return response()->json([
×
74
            'success' => false,
×
75
            'message' => current($validator->getErrors()),
×
76
        ]);
×
77
    }
78

79
    /**
80
     * Связь голоса текущего пользователя (morph-имя relate единое по движку)
81
     *
82
     * @return MorphOne<Poll, Model>
83
     */
84
    private function pollRelation(Model $post): MorphOne
×
85
    {
86
        return $post->morphOne(Poll::class, 'relate')
×
87
            ->where('user_id', getUser('id'));
×
88
    }
89

90
    /**
91
     * Изменяет рейтинг
92
     */
UNCOV
93
    public function rating(Request $request): JsonResponse
×
94
    {
95
        $validTypes = array_merge([
×
96
            Comment::$morphName,
×
97
        ], Registry::$ratingTypes);
×
98

99
        $type = $request->input('type');
×
100
        $vote = $request->input('vote');
×
101

102
        if (! in_array($type, $validTypes, true)) {
×
103
            return response()->json(['success' => false, 'message' => 'Type invalid']);
×
104
        }
105

106
        if (! in_array($vote, ['+', '-'], true)) {
×
107
            return response()->json(['success' => false, 'message' => 'Invalid rating']);
×
108
        }
109

110
        $model = Relation::getMorphedModel($type);
×
111
        $post = $model::query()
×
112
            ->where('id', int($request->input('id')))
×
113
            ->where('user_id', '<>', getUser('id'))
×
114
            ->first();
×
115

116
        if (! $post) {
×
117
            return response()->json(['success' => false, 'message' => __('main.record_not_found')]);
×
118
        }
119

120
        $poll = $this->pollRelation($post)->firstOrNew();
×
121
        $isCancel = false;
×
122

123
        if ($poll->exists) {
×
124
            if ($poll->vote === $vote) {
×
125
                return response()->json(['success' => false]);
×
126
            }
127
            $isCancel = true;
×
128
            $poll->delete();
×
129
        }
130

131
        if (! $isCancel) {
×
132
            $this->pollRelation($post)->create([
×
133
                'user_id'    => getUser('id'),
×
134
                'vote'       => $vote,
×
135
                'created_at' => SITETIME,
×
136
            ]);
×
137
        }
138

139
        $vote === '+' ? $post->increment('rating') : $post->decrement('rating');
×
140
        $post->refresh();
×
141

142
        return response()->json([
×
143
            'success' => true,
×
144
            'cancel'  => $isCancel,
×
145
            'rating'  => formatNum((int) $post->getAttribute('rating'))->toHtml(),
×
146
        ]);
×
147
    }
148

149
    /**
150
     * Загружает файлы
151
     */
152
    public function uploadFile(Request $request, Validator $validator): JsonResponse
×
153
    {
NEW
154
        $imageTypes = Registry::$mediaTypes;
×
155

156
        $fileTypes = array_merge([
×
157
            Comment::$morphName,
×
158
            Message::$morphName,
×
159
        ], Registry::$fileTypes);
×
160

161
        $id = int($request->input('id'));
×
162
        $file = $request->file('file');
×
163
        $type = $request->input('type');
×
164

165
        if (! in_array($type, array_merge($imageTypes, $fileTypes), true)) {
×
166
            return response()->json([
×
167
                'success' => false,
×
168
                'message' => 'Type invalid',
×
169
            ]);
×
170
        }
171

172
        $class = Relation::getMorphedModel($type);
×
173
        $isImageType = in_array($type, $imageTypes, true);
×
174

175
        if ($id) {
×
176
            $model = $class::query()->find($id);
×
177

178
            if (! $model) {
×
179
                return response()->json([
×
180
                    'success' => false,
×
181
                    'message' => 'Service not found',
×
182
                ]);
×
183
            }
184
        } else {
185
            $model = new $class();
×
186
        }
187

NEW
188
        $uploadedFiles = File::query()
×
189
            ->where('relate_type', $type)
×
190
            ->where('relate_id', $id)
×
191
            ->where('user_id', getUser('id'))
×
NEW
192
            ->get(['name']);
×
193

NEW
194
        $duplicate = $file && $uploadedFiles->contains(
×
NEW
195
            'name',
×
NEW
196
            Str::substr(getBodyName($file->getClientOriginalName()), 0, 50) . '.' . strtolower($file->getClientOriginalExtension())
×
NEW
197
        );
×
198

199
        $validator
×
NEW
200
            ->lt($uploadedFiles->count(), setting('maxfiles'), __('validator.files_max', ['max' => setting('maxfiles')]))
×
201
            ->false($duplicate, __('validator.file_duplicate'));
×
202

203
        if ($model->id) {
×
204
            $validator->true($model->user_id === getUser('id') || isAdmin(), __('ajax.record_not_author'));
×
205
        }
206

207
        if ($validator->isValid()) {
×
208
            $allowedExt = setting($isImageType ? 'media_extensions' : 'file_extensions');
×
209

210
            $rules = [
×
211
                'minweight'  => 100,
×
212
                'maxsize'    => setting('filesize'),
×
213
                'extensions' => explode(',', $allowedExt),
×
214
            ];
×
215

216
            $validator->file($file, $rules, __('validator.file_upload_failed'));
×
217
        }
218

219
        if ($validator->isValid()) {
×
220
            $fileData = $model->uploadFile($file);
×
221
            if (method_exists($model, 'convertVideo')) {
×
222
                $model->convertVideo($fileData);
×
223
            }
224

225
            if ($isImageType) {
×
226
                $data = [
×
227
                    'success' => true,
×
228
                    'id'      => $fileData['id'],
×
229
                    'path'    => $fileData['path'],
×
230
                    'type'    => $fileData['type'],
×
231
                ];
×
232
            } else {
233
                if (method_exists($model, 'addFileToArchive')) {
×
234
                    $model->addFileToArchive($fileData);
×
235
                }
236

237
                $data = [
×
238
                    'success' => true,
×
239
                    'id'      => $fileData['id'],
×
240
                    'path'    => $fileData['path'],
×
241
                    'name'    => $fileData['name'],
×
242
                    'size'    => $fileData['size'],
×
243
                    'type'    => $fileData['type'],
×
244
                ];
×
245
            }
246

247
            return response()->json($data);
×
248
        }
249

250
        return response()->json([
×
251
            'success' => false,
×
252
            'message' => current($validator->getErrors()),
×
253
        ]);
×
254
    }
255

256
    /**
257
     * Удаляет файлы
258
     */
259
    public function deleteFile(Request $request, Validator $validator): JsonResponse
×
260
    {
261
        $types = array_merge([
×
262
            Comment::$morphName,
×
263
            Message::$morphName,
×
264
        ], Registry::$mediaTypes, Registry::$fileTypes);
×
265

266
        $id = int($request->input('id'));
×
267
        $type = $request->input('type');
×
268

269
        if (! in_array($type, $types, true)) {
×
270
            return response()->json([
×
271
                'success' => false,
×
272
                'message' => 'Type invalid',
×
273
            ]);
×
274
        }
275

276
        $file = File::query()
×
277
            ->where('relate_type', $type)
×
278
            ->find($id);
×
279

280
        if (! $file) {
×
281
            return response()->json([
×
282
                'success' => false,
×
283
                'message' => 'File not found',
×
284
            ]);
×
285
        }
286

287
        $validator
×
288
            ->true($file->user_id === getUser('id') || isAdmin(), __('ajax.record_not_author'));
×
289

290
        if ($validator->isValid()) {
×
291
            $file->delete();
×
292

293
            return response()->json([
×
294
                'success' => true,
×
295
                'path'    => $file->path,
×
296
            ]);
×
297
        }
298

299
        return response()->json([
×
300
            'success' => false,
×
301
            'message' => current($validator->getErrors()),
×
302
        ]);
×
303
    }
304

305
    /**
306
     * Возвращает список стикеров
307
     */
308
    public function getStickers(): JsonResponse
×
309
    {
310
        $stickers = Sticker::query()
×
311
            ->with('category:id,name')
×
312
            ->orderBy(DB::raw('CHAR_LENGTH(code)'))
×
313
            ->orderBy('name')
×
314
            ->get(['id', 'category_id', 'code', 'name']);
×
315

316
        $grouped = $stickers
×
317
            ->groupBy('category_id')
×
318
            ->toBase()
×
319
            ->map(fn ($items, $categoryId) => [
×
320
                'id'       => (int) $categoryId,
×
321
                'name'     => $items->first()->category->name,
×
322
                'stickers' => $items->map(fn (Sticker $s) => ['code' => $s->code, 'name' => $s->name])->values()->all(),
×
323
            ])
×
324
            ->values();
×
325

326
        return response()->json($grouped);
×
327
    }
328

329
    /**
330
     * Резолв прямой ссылки на картинку через og:image
331
     */
332
    public function resolveImage(Request $request): JsonResponse
×
333
    {
334
        $url = filter_var((string) $request->input('url'), FILTER_VALIDATE_URL);
×
335

336
        if (! $url) {
×
337
            return response()->json(['image' => null]);
×
338
        }
339

340
        $ctx = stream_context_create(['http' => [
×
341
            'timeout'         => 5,
×
342
            'follow_location' => true,
×
343
            'user_agent'      => 'Mozilla/5.0',
×
344
        ]]);
×
345

346
        $html = @file_get_contents($url, false, $ctx);
×
347

348
        if ($html && preg_match('/<meta[^>]+property=["\']og:image["\'][^>]+content=["\'](https?:[^"\']+)["\']/i', $html, $m)) {
×
349
            return response()->json(['image' => $m[1]]);
×
350
        }
351

352
        return response()->json(['image' => null]);
×
353
    }
354

355
    /**
356
     * Set theme
357
     */
358
    public function setTheme(Request $request): JsonResponse
×
359
    {
360
        cookie()->queue(
×
361
            cookie()->forever(
×
362
                'theme',
×
363
                $request->input('theme') === 'dark' ? 'dark' : 'light',
×
364
            )
×
365
        );
×
366

367
        return response()->json([
×
368
            'success' => true,
×
369
        ]);
×
370
    }
371
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc