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

visavi / rotor / 26541133708

27 May 2026 09:56PM UTC coverage: 14.548% (-0.04%) from 14.587%
26541133708

push

github

visavi
Поправил обновление модуля, добавил ошибки при конфликте модулей, обновление рееста и скачивание файла

0 of 29 new or added lines in 3 files covered. (0.0%)

8 existing lines in 2 files now uncovered.

872 of 5994 relevant lines covered (14.55%)

1.09 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

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

NEW
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
        return view('admin/modules/marketplace', compact('available', 'modules', 'moduleNames'));
×
176
    }
177

178
    /**
179
     * Форма загрузки модуля
180
     */
181
    public function upload(): View
×
182
    {
183
        return view('admin/modules/upload');
×
184
    }
185

186
    /**
187
     * Установка модуля из ZIP-файла
188
     */
189
    public function uploadZip(Request $request): RedirectResponse
×
190
    {
191
        if (! $request->hasFile('zip') || ! $request->file('zip')->isValid()) {
×
192
            setFlash('danger', __('admin.modules.upload_invalid_file'));
×
193

194
            return redirect()->route('admin.modules.upload');
×
195
        }
196

197
        try {
198
            $moduleName = $this->extractZip($request->file('zip')->getPathname());
×
199
        } catch (\Exception $e) {
×
200
            setFlash('danger', $e->getMessage());
×
201

202
            return redirect()->route('admin.modules.upload');
×
203
        }
204

NEW
205
        $isUpdate = Module::query()->where('name', $moduleName)->exists();
×
NEW
206
        $redirect = $isUpdate
×
NEW
207
            ? '/admin/modules/install?module=' . $moduleName . '&update=1'
×
NEW
208
            : '/admin/modules/module?module=' . $moduleName;
×
209

NEW
210
        return redirect($redirect)
×
UNCOV
211
            ->with('success', __('admin.modules.upload_success_extracted'));
×
212
    }
213

214
    /**
215
     * Установка модуля по URL
216
     */
217
    public function download(Request $request): RedirectResponse
×
218
    {
219
        $url = trim($request->input('url', ''));
×
220

221
        if (! filter_var($url, FILTER_VALIDATE_URL)) {
×
222
            setFlash('danger', __('admin.modules.download_invalid_url'));
×
223

NEW
224
            return redirect()->back();
×
225
        }
226

227
        try {
228
            $response = Http::timeout(30)->get($url);
×
229

230
            if (! $response->ok()) {
×
231
                setFlash('danger', __('admin.modules.download_failed'));
×
232

NEW
233
                return redirect()->back();
×
234
            }
235

236
            $tempDir = storage_path('app/temp');
×
237
            if (! is_dir($tempDir)) {
×
238
                mkdir($tempDir, 0755, true);
×
239
            }
240
            $tempFile = $tempDir . '/rotor_module_' . uniqid() . '.zip';
×
241
            file_put_contents($tempFile, $response->body());
×
242

243
            $moduleName = $this->extractZip($tempFile);
×
244
            @unlink($tempFile);
×
245
        } catch (\Exception $e) {
×
246
            setFlash('danger', $e->getMessage());
×
247

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

NEW
251
        $isUpdate = Module::query()->where('name', $moduleName)->exists();
×
NEW
252
        $redirect = $isUpdate
×
NEW
253
            ? '/admin/modules/install?module=' . $moduleName . '&update=1'
×
NEW
254
            : '/admin/modules/module?module=' . $moduleName;
×
255

NEW
256
        return redirect($redirect)
×
UNCOV
257
            ->with('success', __('admin.modules.upload_success_extracted'));
×
258
    }
259

260
    /**
261
     * Распаковка ZIP-архива модуля
262
     */
263
    private function extractZip(string $zipPath): string
×
264
    {
265
        $zip = new ZipArchive();
×
266

267
        if ($zip->open($zipPath) !== true) {
×
268
            throw new \RuntimeException(__('admin.modules.zip_open_failed'));
×
269
        }
270

UNCOV
271
        $topDirs = [];
×
272
        for ($i = 0; $i < $zip->numFiles; $i++) {
×
273
            $name = $zip->getNameIndex($i);
×
274

275
            if (str_contains($name, '..')) {
×
276
                $zip->close();
×
277
                throw new \RuntimeException(__('admin.modules.zip_invalid_path'));
×
278
            }
279

280
            $parts = explode('/', $name);
×
281
            if ($parts[0] !== '') {
×
282
                $topDirs[$parts[0]] = true;
×
283
            }
284
        }
285

286
        if (count($topDirs) !== 1) {
×
287
            $zip->close();
×
288
            throw new \RuntimeException(__('admin.modules.zip_invalid_structure'));
×
289
        }
290

291
        $moduleName = array_key_first($topDirs);
×
292

293
        if (! preg_match('/^[A-Z][A-Za-z0-9]+$/', $moduleName)) {
×
294
            $zip->close();
×
295
            throw new \RuntimeException(__('admin.modules.zip_invalid_name'));
×
296
        }
297

298
        $targetPath = base_path('modules/' . $moduleName);
×
UNCOV
299
        $zip->extractTo(base_path('modules/'));
×
300
        $zip->close();
×
301

302
        $this->chmodRecursive($targetPath);
×
303

304
        if (! file_exists($targetPath . '/module.php')) {
×
305
            // Откат: удалить распакованное
306
            $this->deleteDirectory($targetPath);
×
307
            throw new \RuntimeException(__('admin.modules.zip_no_module_file'));
×
308
        }
309

310
        return $moduleName;
×
311
    }
312

313
    /**
314
     * Удаление файлов модуля с диска
315
     */
316
    public function deleteFiles(Request $request): RedirectResponse
×
317
    {
318
        $moduleName = $request->input('module');
×
319
        $modulePath = base_path('modules/' . $moduleName);
×
320

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

325
        if (Module::query()->where('name', $moduleName)->exists()) {
×
326
            abort(200, __('admin.modules.delete_files_not_uninstalled'));
×
327
        }
328

329
        $this->deleteDirectory($modulePath);
×
330

331
        Artisan::call('route:clear');
×
332
        setFlash('success', __('admin.modules.module_files_deleted'));
×
333

334
        return redirect()->route('admin.modules.index');
×
335
    }
336

337
    private function chmodRecursive(string $path): void
×
338
    {
339
        chmod($path, 0755);
×
340

341
        foreach (scandir($path) as $item) {
×
342
            if ($item === '.' || $item === '..') {
×
343
                continue;
×
344
            }
345

346
            $full = $path . '/' . $item;
×
347
            if (is_dir($full)) {
×
348
                $this->chmodRecursive($full);
×
349
            } else {
350
                chmod($full, 0644);
×
351
            }
352
        }
353
    }
354

355
    private function deleteDirectory(string $path): void
×
356
    {
357
        if (is_link($path)) {
×
358
            unlink($path);
×
359

360
            return;
×
361
        }
362

363
        if (! is_dir($path)) {
×
364
            return;
×
365
        }
366

367
        foreach (scandir($path) as $item) {
×
368
            if ($item === '.' || $item === '..') {
×
369
                continue;
×
370
            }
371

372
            $full = $path . '/' . $item;
×
373
            is_dir($full) && ! is_link($full) ? $this->deleteDirectory($full) : unlink($full);
×
374
        }
375

376
        rmdir($path);
×
377
    }
378

379
    /**
380
     * Удаление/Выключение модуля
381
     */
382
    public function uninstall(Request $request): RedirectResponse
×
383
    {
384
        $moduleName = $request->input('module');
×
385
        $disable = int($request->input('disable'));
×
386
        $modulePath = base_path('modules/' . $moduleName);
×
387

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

392
        $module = Module::query()->where('name', $moduleName)->first();
×
393
        if (! $module) {
×
394
            abort(200, __('admin.modules.module_not_found'));
×
395
        }
396

397
        $module->deleteSymlink();
×
398
        Artisan::call('route:clear');
×
399

400
        if ($disable) {
×
401
            $module->update([
×
402
                'active'     => false,
×
403
                'updated_at' => SITETIME,
×
404
            ]);
×
405
            $result = __('admin.modules.module_success_disabled');
×
406
        } else {
407
            if (env('MODULES_SAFE_MODE', false)) {
×
408
                setFlash('danger', __('admin.modules.safe_mode_enabled'));
×
409

410
                return redirect('admin/modules/module?module=' . $moduleName);
×
411
            }
412

413
            $module->rollback();
×
414
            $module->delete();
×
415
            $result = __('admin.modules.module_success_deleted');
×
416
        }
417

418
        clearCache('modules');
×
419
        setFlash('success', $result);
×
420

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