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

visavi / rotor / 26723576697

31 May 2026 08:27PM UTC coverage: 14.172% (-0.4%) from 14.618%
26723576697

push

github

visavi
Перенес файлы обновлений бд в отдельную папку

785 of 5539 relevant lines covered (14.17%)

1.26 hits per line

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

0.0
/app/Http/Controllers/Admin/ModuleController.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Http\Controllers\Admin;
6

7
use App\Models\Module;
8
use App\Models\ModuleRegistry;
9
use Illuminate\Http\RedirectResponse;
10
use Illuminate\Http\Request;
11
use Illuminate\Support\Facades\Artisan;
12
use Illuminate\Support\Facades\Http;
13
use Illuminate\View\View;
14
use ZipArchive;
15

16
class ModuleController extends AdminController
17
{
18
    /**
19
     * Главная страница
20
     */
21
    public function index(): View
×
22
    {
23
        $modules = Module::query()->get();
×
24
        $moduleInstall = [];
×
25
        foreach ($modules as $module) {
×
26
            $moduleInstall[$module->name] = $module;
×
27
        }
28

29
        $moduleNames = [];
×
30
        $modulesLoaded = glob(base_path('modules/*'), GLOB_ONLYDIR);
×
31
        foreach ($modulesLoaded as $module) {
×
32
            if (file_exists($module . '/module.php')) {
×
33
                $moduleNames[basename($module)] = include $module . '/module.php';
×
34
            }
35
        }
36

37
        $installed = array_intersect_key($moduleInstall, $moduleNames);
×
38
        $counts = [
×
39
            'all'           => count($moduleNames),
×
40
            'installed'     => count(array_filter($installed, fn ($m) => $m->active)),
×
41
            'disabled'      => count(array_filter($installed, fn ($m) => ! $m->active)),
×
42
            'not-installed' => count($moduleNames) - count($installed),
×
43
        ];
×
44

45
        $registryModules = ModuleRegistry::getAvailableModules();
×
46

47
        return view('admin/modules/index', compact('moduleInstall', 'moduleNames', 'counts', 'registryModules'));
×
48
    }
49

50
    /**
51
     * Просмотр модуля
52
     */
53
    public function module(Request $request): View
×
54
    {
55
        $moduleName = (string) $request->input('module');
×
56
        $modulePath = base_path('modules/' . $moduleName);
×
57

58
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
×
59
            abort(200, __('admin.modules.module_not_found'));
×
60
        }
61

62
        $moduleConfig = include $modulePath . '/module.php';
×
63
        $module = Module::query()->where('name', $moduleName)->first();
×
64

65
        if (file_exists($modulePath . '/screenshots')) {
×
66
            $moduleConfig['screenshots'] = glob($modulePath . '/screenshots/*.{gif,png,jpg,jpeg,webp}', GLOB_BRACE);
×
67
        }
68

69
        if (file_exists($modulePath . '/database/migrations')) {
×
70
            $moduleConfig['migrations'] = array_map('basename', glob($modulePath . '/database/migrations/*.php'));
×
71
        }
72

73
        if (file_exists($modulePath . '/resources/assets')) {
×
74
            $moduleConfig['symlink'] = Module::getLinkNameByPath($modulePath);
×
75
        }
76

77
        if (file_exists($modulePath . '/config.php')) {
×
78
            $moduleConfig['config'] = file_get_contents($modulePath . '/config.php');
×
79
        }
80

81
        if ($module && $module->settings) {
×
82
            $moduleConfig['settings'] = var_export($module->settings, true);
×
83
        }
84

85
        if (file_exists($modulePath . '/routes.php')) {
×
86
            $moduleConfig['routes'] = file_get_contents($modulePath . '/routes.php');
×
87
        }
88

89
        if (file_exists($modulePath . '/hooks.php')) {
×
90
            $moduleConfig['hooks'] = file_get_contents($modulePath . '/hooks.php');
×
91
        }
92

93
        if (file_exists($modulePath . '/helpers.php')) {
×
94
            $moduleConfig['helpers'] = file_get_contents($modulePath . '/helpers.php');
×
95
        }
96

97
        if (file_exists($modulePath . '/middleware.php')) {
×
98
            $moduleConfig['middleware'] = file_get_contents($modulePath . '/middleware.php');
×
99
        }
100

101
        $registryInfo = ModuleRegistry::getAvailableModules()[$moduleName] ?? null;
×
102

103
        return view('admin/modules/module', compact('module', 'moduleConfig', 'moduleName', 'registryInfo'));
×
104
    }
105

106
    /**
107
     * Установка модуля
108
     */
109
    public function install(Request $request): RedirectResponse
×
110
    {
111
        $moduleName = $request->input('module');
×
112
        $enable = int($request->input('enable'));
×
113
        $update = int($request->input('update'));
×
114
        $modulePath = base_path('modules/' . $moduleName);
×
115

116
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
×
117
            abort(200, __('admin.modules.module_not_found'));
×
118
        }
119

120
        $module = Module::query()->firstOrNew(['name' => $moduleName]);
×
121

122
        $moduleConfig = include $modulePath . '/module.php';
×
123
        $module->createSymlink();
×
124
        $module->migrate();
×
125

126
        Artisan::call('route:clear');
×
127
        $result = __('admin.modules.module_success_installed');
×
128

129
        if ($module->exists) {
×
130
            if ($update) {
×
131
                $module->update([
×
132
                    'version'    => $moduleConfig['version'],
×
133
                    'updated_at' => SITETIME,
×
134
                ]);
×
135
                $result = __('admin.modules.module_success_updated');
×
136
            }
137

138
            if ($enable) {
×
139
                $module->update([
×
140
                    'active'     => true,
×
141
                    'updated_at' => SITETIME,
×
142
                ]);
×
143
                $result = __('admin.modules.module_success_enabled');
×
144
            }
145
        } else {
146
            $module->fill([
×
147
                'version'    => $moduleConfig['version'],
×
148
                'updated_at' => SITETIME,
×
149
                'created_at' => SITETIME,
×
150
            ])->save();
×
151
        }
152

153
        clearCache('modules');
×
154
        setFlash('success', $result);
×
155

156
        return redirect('admin/modules/module?module=' . $moduleName);
×
157
    }
158

159
    /**
160
     * Каталог модулей из реестров
161
     */
162
    public function marketplace(Request $request): View
×
163
    {
164
        $force = (bool) $request->input('refresh');
×
165
        $available = ModuleRegistry::getAvailableModules($force);
×
166

167
        $modules = Module::query()->get()->keyBy('name');
×
168
        $moduleNames = [];
×
169

170
        $modulesLoaded = glob(base_path('modules/*'), GLOB_ONLYDIR);
×
171
        foreach ($modulesLoaded as $module) {
×
172
            $moduleNames[] = basename($module);
×
173
        }
174

175
        $counts = ['all' => count($available), 'installed' => 0, 'disabled' => 0, 'not-installed' => 0];
×
176
        foreach ($available as $name => $info) {
×
177
            $localExists = in_array($name, $moduleNames, true);
×
178
            $installed = $modules->has($name) && $localExists;
×
179

180
            if ($installed) {
×
181
                $counts[$modules[$name]->active ? 'installed' : 'disabled']++;
×
182
            } else {
183
                $counts['not-installed']++;
×
184
            }
185
        }
186

187
        return view('admin/modules/marketplace', compact('available', 'modules', 'moduleNames', 'counts'));
×
188
    }
189

190
    /**
191
     * Форма загрузки модуля
192
     */
193
    public function upload(): View
×
194
    {
195
        return view('admin/modules/upload');
×
196
    }
197

198
    /**
199
     * Установка модуля из ZIP-файла
200
     */
201
    public function uploadZip(Request $request): RedirectResponse
×
202
    {
203
        if (! $request->hasFile('zip') || ! $request->file('zip')->isValid()) {
×
204
            setFlash('danger', __('admin.modules.upload_invalid_file'));
×
205

206
            return redirect()->route('admin.modules.upload');
×
207
        }
208

209
        try {
210
            $moduleName = $this->extractZip($request->file('zip')->getPathname());
×
211
        } catch (\Exception $e) {
×
212
            setFlash('danger', $e->getMessage());
×
213

214
            return redirect()->route('admin.modules.upload');
×
215
        }
216

217
        $isUpdate = Module::query()->where('name', $moduleName)->exists();
×
218
        $redirect = $isUpdate
×
219
            ? '/admin/modules/install?module=' . $moduleName . '&update=1'
×
220
            : '/admin/modules/module?module=' . $moduleName;
×
221

222
        return redirect($redirect)
×
223
            ->with('success', __('admin.modules.upload_success_extracted'));
×
224
    }
225

226
    /**
227
     * Установка модуля по URL
228
     */
229
    public function download(Request $request): RedirectResponse
×
230
    {
231
        $url = trim($request->input('url', ''));
×
232

233
        if (! filter_var($url, FILTER_VALIDATE_URL)) {
×
234
            setFlash('danger', __('admin.modules.download_invalid_url'));
×
235

236
            return redirect()->back();
×
237
        }
238

239
        $maxSize = (int) setting('filesize') * 1024;
×
240

241
        try {
242
            $response = Http::timeout(30)->get($url);
×
243

244
            if (! $response->ok()) {
×
245
                setFlash('danger', __('admin.modules.download_failed'));
×
246

247
                return redirect()->back();
×
248
            }
249

250
            $body = $response->body();
×
251

252
            if (substr($body, 0, 4) !== "PK\x03\x04") {
×
253
                setFlash('danger', __('admin.modules.download_not_zip'));
×
254

255
                return redirect()->back();
×
256
            }
257

258
            $contentLength = (int) $response->header('Content-Length');
×
259
            if (($contentLength > 0 && $contentLength > $maxSize) || strlen($body) > $maxSize) {
×
260
                setFlash('danger', __('admin.modules.download_too_large', ['size' => formatSize($maxSize)]));
×
261

262
                return redirect()->back();
×
263
            }
264

265
            $tempDir = storage_path('app/temp');
×
266
            if (! is_dir($tempDir)) {
×
267
                mkdir($tempDir, 0755, true);
×
268
            }
269
            $tempFile = $tempDir . '/rotor_module_' . uniqid() . '.zip';
×
270
            file_put_contents($tempFile, $body);
×
271

272
            try {
273
                $moduleName = $this->extractZip($tempFile);
×
274
            } finally {
275
                @unlink($tempFile);
×
276
            }
277
        } catch (\Exception $e) {
×
278
            setFlash('danger', $e->getMessage());
×
279

280
            return redirect()->back();
×
281
        }
282

283
        $isUpdate = Module::query()->where('name', $moduleName)->exists();
×
284
        $redirect = $isUpdate
×
285
            ? '/admin/modules/install?module=' . $moduleName . '&update=1'
×
286
            : '/admin/modules/module?module=' . $moduleName;
×
287

288
        return redirect($redirect)
×
289
            ->with('success', __('admin.modules.upload_success_extracted'));
×
290
    }
291

292
    /**
293
     * Распаковка ZIP-архива модуля
294
     */
295
    private function extractZip(string $zipPath): string
×
296
    {
297
        $zip = new ZipArchive();
×
298

299
        if ($zip->open($zipPath) !== true) {
×
300
            throw new \RuntimeException(__('admin.modules.zip_open_failed'));
×
301
        }
302

303
        $topDirs = [];
×
304
        for ($i = 0; $i < $zip->numFiles; $i++) {
×
305
            $name = $zip->getNameIndex($i);
×
306

307
            if (str_contains($name, '..')) {
×
308
                $zip->close();
×
309
                throw new \RuntimeException(__('admin.modules.zip_invalid_path'));
×
310
            }
311

312
            $parts = explode('/', $name);
×
313
            if ($parts[0] !== '') {
×
314
                $topDirs[$parts[0]] = true;
×
315
            }
316
        }
317

318
        if (count($topDirs) !== 1) {
×
319
            $zip->close();
×
320
            throw new \RuntimeException(__('admin.modules.zip_invalid_structure'));
×
321
        }
322

323
        $moduleName = array_key_first($topDirs);
×
324

325
        if (! preg_match('/^[A-Z][A-Za-z0-9]+$/', $moduleName)) {
×
326
            $zip->close();
×
327
            throw new \RuntimeException(__('admin.modules.zip_invalid_name'));
×
328
        }
329

330
        $targetPath = base_path('modules/' . $moduleName);
×
331

332
        // Существующий модуль уводим в резервную копию, чтобы чистая распаковка
333
        // не оставила старых файлов и можно было откатиться при сбое
334
        $backupPath = null;
×
335
        if (is_dir($targetPath)) {
×
336
            $backupPath = base_path('modules/.backup_' . $moduleName . '_' . time());
×
337
            if (! rename($targetPath, $backupPath)) {
×
338
                $zip->close();
×
339
                throw new \RuntimeException(__('admin.modules.zip_backup_failed'));
×
340
            }
341
        }
342

343
        if (! $zip->extractTo(base_path('modules/'))) {
×
344
            $zip->close();
×
345
            $this->restoreBackup($targetPath, $backupPath);
×
346
            throw new \RuntimeException(__('admin.modules.zip_extract_failed'));
×
347
        }
348
        $zip->close();
×
349

350
        $this->chmodRecursive($targetPath);
×
351

352
        if (! file_exists($targetPath . '/module.php')) {
×
353
            $this->restoreBackup($targetPath, $backupPath);
×
354
            throw new \RuntimeException(__('admin.modules.zip_no_module_file'));
×
355
        }
356

357
        if ($backupPath) {
×
358
            $this->deleteDirectory($backupPath);
×
359
        }
360

361
        return $moduleName;
×
362
    }
363

364
    /**
365
     * Удаление файлов модуля с диска
366
     */
367
    public function deleteFiles(Request $request): RedirectResponse
×
368
    {
369
        $moduleName = $request->input('module');
×
370
        $modulePath = base_path('modules/' . $moduleName);
×
371

372
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
×
373
            abort(200, __('admin.modules.module_not_found'));
×
374
        }
375

376
        if (Module::query()->where('name', $moduleName)->exists()) {
×
377
            abort(200, __('admin.modules.delete_files_not_uninstalled'));
×
378
        }
379

380
        $this->deleteDirectory($modulePath);
×
381

382
        Artisan::call('route:clear');
×
383
        setFlash('success', __('admin.modules.module_files_deleted'));
×
384

385
        return redirect()->route('admin.modules.index');
×
386
    }
387

388
    /**
389
     * Откат распаковки: удалить частично распакованное и вернуть резервную копию
390
     */
391
    private function restoreBackup(string $targetPath, ?string $backupPath): void
×
392
    {
393
        $this->deleteDirectory($targetPath);
×
394

395
        if ($backupPath && is_dir($backupPath)) {
×
396
            rename($backupPath, $targetPath);
×
397
        }
398
    }
399

400
    private function chmodRecursive(string $path): void
×
401
    {
402
        chmod($path, 0755);
×
403

404
        foreach (scandir($path) as $item) {
×
405
            if ($item === '.' || $item === '..') {
×
406
                continue;
×
407
            }
408

409
            $full = $path . '/' . $item;
×
410
            if (is_dir($full)) {
×
411
                $this->chmodRecursive($full);
×
412
            } else {
413
                chmod($full, 0644);
×
414
            }
415
        }
416
    }
417

418
    private function deleteDirectory(string $path): void
×
419
    {
420
        if (is_link($path)) {
×
421
            unlink($path);
×
422

423
            return;
×
424
        }
425

426
        if (! is_dir($path)) {
×
427
            return;
×
428
        }
429

430
        foreach (scandir($path) as $item) {
×
431
            if ($item === '.' || $item === '..') {
×
432
                continue;
×
433
            }
434

435
            $full = $path . '/' . $item;
×
436
            is_dir($full) && ! is_link($full) ? $this->deleteDirectory($full) : unlink($full);
×
437
        }
438

439
        rmdir($path);
×
440
    }
441

442
    /**
443
     * Удаление/Выключение модуля
444
     */
445
    public function uninstall(Request $request): RedirectResponse
×
446
    {
447
        $moduleName = $request->input('module');
×
448
        $disable = int($request->input('disable'));
×
449
        $modulePath = base_path('modules/' . $moduleName);
×
450

451
        if (! preg_match('|^[A-Z][\w\-]+$|', $moduleName) || ! file_exists($modulePath)) {
×
452
            abort(200, __('admin.modules.module_not_found'));
×
453
        }
454

455
        $module = Module::query()->where('name', $moduleName)->first();
×
456
        if (! $module) {
×
457
            abort(200, __('admin.modules.module_not_found'));
×
458
        }
459

460
        $module->deleteSymlink();
×
461
        Artisan::call('route:clear');
×
462

463
        if ($disable) {
×
464
            $module->update([
×
465
                'active'     => false,
×
466
                'updated_at' => SITETIME,
×
467
            ]);
×
468
            $result = __('admin.modules.module_success_disabled');
×
469
        } else {
470
            if (env('MODULES_SAFE_MODE', false)) {
×
471
                setFlash('danger', __('admin.modules.safe_mode_enabled'));
×
472

473
                return redirect('admin/modules/module?module=' . $moduleName);
×
474
            }
475

476
            $module->rollback();
×
477
            $module->delete();
×
478
            $result = __('admin.modules.module_success_deleted');
×
479
        }
480

481
        clearCache('modules');
×
482
        setFlash('success', $result);
×
483

484
        return redirect('admin/modules/module?module=' . $moduleName);
×
485
    }
486
}
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