1: <?php
2: namespace Opencart\Admin\Controller\Common;
3: /**
4: * Class Security
5: *
6: * @package Opencart\Admin\Controller\Common
7: */
8: class Security extends \Opencart\System\Engine\Controller {
9: /**
10: * Index
11: *
12: * @return string
13: */
14: public function index(): string {
15: $this->load->language('common/security');
16:
17: $data['list'] = $this->controller_common_security->getList();
18:
19: $data['user_token'] = $this->session->data['user_token'];
20:
21: return $this->load->view('common/security', $data);
22: }
23:
24: /**
25: * List
26: *
27: * @return void
28: */
29: public function list(): void {
30: $this->load->language('common/security');
31:
32: $this->response->setOutput($this->controller_common_security->getList());
33: }
34:
35: /**
36: * getList
37: *
38: * @return string
39: */
40: public function getList(): string {
41: // Install directory exists
42: $path = DIR_OPENCART . 'install/';
43:
44: if (is_dir($path)) {
45: $data['install'] = $path;
46: } else {
47: $data['install'] = '';
48: }
49:
50: // Storage directory exists
51: $path = DIR_SYSTEM . 'storage/';
52:
53: if (DIR_STORAGE == $path) {
54: $data['storage'] = $path;
55:
56: $data['document_root'] = str_replace('\\', '/', realpath($this->request->server['DOCUMENT_ROOT'] . '/../')) . '/';
57:
58: $path = '';
59:
60: $data['paths'] = [];
61:
62: $parts = explode('/', rtrim($data['document_root'], '/'));
63:
64: foreach ($parts as $part) {
65: $path .= $part . '/';
66:
67: $data['paths'][] = $path;
68: }
69:
70: rsort($data['paths']);
71: } else {
72: $data['storage'] = '';
73: }
74:
75: // Storage delete
76: $path = DIR_SYSTEM . 'storage/';
77:
78: if (is_dir($path) && DIR_STORAGE != $path) {
79: $data['storage_delete'] = $path;
80: } else {
81: $data['storage_delete'] = '';
82: }
83:
84: // Check admin directory ia renamed
85: $path = DIR_OPENCART . 'admin/';
86:
87: if (DIR_APPLICATION == $path) {
88: $data['admin'] = 'admin';
89: } else {
90: $data['admin'] = '';
91: }
92:
93: // Admin delete
94: $path = DIR_OPENCART . 'admin/';
95:
96: if (is_dir($path) && DIR_APPLICATION != $path) {
97: $data['admin_delete'] = $path;
98: } else {
99: $data['admin_delete'] = '';
100: }
101:
102: $data['user_token'] = $this->session->data['user_token'];
103:
104: if ($data['install'] || $data['storage'] || $data['storage_delete'] || $data['admin'] || $data['admin_delete']) {
105: return $this->load->view('common/security_list', $data);
106: } else {
107: return '';
108: }
109: }
110:
111: /**
112: * Install
113: *
114: * @return void
115: */
116: public function install(): void {
117: $this->load->language('common/security');
118:
119: $json = [];
120:
121: if (!$this->user->hasPermission('modify', 'common/security')) {
122: $json['error'] = $this->language->get('error_permission');
123: }
124:
125: if (!$json) {
126: if (!is_dir(DIR_OPENCART . 'install/')) {
127: $json['error'] = $this->language->get('error_install');
128: }
129: }
130:
131: if (!$json) {
132: $files = [];
133:
134: $path = DIR_OPENCART . 'install/';
135:
136: // Make path into an array
137: $directory = [$path];
138:
139: // While the path array is still populated keep looping through
140: while (count($directory) != 0) {
141: $next = array_shift($directory);
142:
143: if (is_dir($next)) {
144: foreach (glob(rtrim($next, '/') . '/{*,.[!.]*,..?*}', GLOB_BRACE) as $file) {
145: // If directory add to path array
146: if (is_dir($file)) {
147: $directory[] = $file;
148: }
149:
150: // Add the file to the files to be deleted array
151: $files[] = $file;
152: }
153: }
154: }
155:
156: rsort($files);
157:
158: foreach ($files as $file) {
159: if (is_file($file)) {
160: unlink($file);
161: } elseif (is_dir($file)) {
162: rmdir($file);
163: }
164: }
165:
166: rmdir($path);
167:
168: $json['success'] = $this->language->get('text_install_success');
169: }
170:
171: $this->response->addHeader('Content-Type: application/json');
172: $this->response->setOutput(json_encode($json));
173: }
174:
175: /**
176: * Storage
177: *
178: * @return void
179: */
180: public function storage(): void {
181: $this->load->language('common/security');
182:
183: $json = [];
184:
185: if (isset($this->request->get['page'])) {
186: $page = (int)$this->request->get['page'];
187: } else {
188: $page = 1;
189: }
190:
191: if (isset($this->request->get['name'])) {
192: $name = preg_replace('/[^a-zA-Z0-9_\.]/', '', basename(html_entity_decode(trim($this->request->get['name']), ENT_QUOTES, 'UTF-8')));
193: } else {
194: $name = '';
195: }
196:
197: if (isset($this->request->get['path'])) {
198: $path = preg_replace('/[^a-zA-Z0-9_\:\/\.]/', '', html_entity_decode(trim($this->request->get['path']), ENT_QUOTES, 'UTF-8'));
199: } else {
200: $path = '';
201: }
202:
203: if (!$this->user->hasPermission('modify', 'common/security')) {
204: $json['error'] = $this->language->get('error_permission');
205: }
206:
207: if (!$json) {
208: $base_old = DIR_STORAGE;
209: $base_new = $path . $name . '/';
210:
211: // Check current storage path exists
212: if (!is_dir($base_old)) {
213: $json['error'] = $this->language->get('error_storage');
214: }
215:
216: // Check the chosen directory is not in the public webspace
217: $root = str_replace('\\', '/', realpath($this->request->server['DOCUMENT_ROOT'] . '/../'));
218:
219: if ((substr($base_new, 0, strlen($root)) != $root) || ($root == $base_new)) {
220: $json['error'] = $this->language->get('error_storage_root');
221: }
222:
223: if (!str_starts_with($name, 'storage')) {
224: $json['error'] = $this->language->get('error_storage_name');
225: }
226:
227: if (!is_writable(DIR_OPENCART . 'config.php') || !is_writable(DIR_APPLICATION . 'config.php')) {
228: $json['error'] = $this->language->get('error_writable');
229: }
230: }
231:
232: if (!$json) {
233: $files = [];
234:
235: // Make path into an array
236: $directory = [$base_old];
237:
238: // While the path array is still populated keep looping through
239: while (count($directory) != 0) {
240: $next = array_shift($directory);
241:
242: foreach (glob(rtrim($next, '/') . '/{*,.[!.]*,..?*}', GLOB_BRACE) as $file) {
243: // If directory add to path array
244: if (is_dir($file)) {
245: $directory[] = $file;
246: }
247:
248: // Add the file to the files to be deleted array
249: $files[] = $file;
250: }
251: }
252:
253: // Create the new storage folder
254: if (!is_dir($base_new)) {
255: mkdir($base_new, 0777);
256: }
257:
258: // Copy the
259: $total = count($files);
260: $limit = 200;
261:
262: $start = ($page - 1) * $limit;
263: $end = ($start > ($total - $limit)) ? $total : ($start + $limit);
264:
265: for ($i = $start; $i < $end; $i++) {
266: $destination = substr($files[$i], strlen($base_old));
267:
268: // Must not have a path before files and directories can be moved
269: $path_new = '';
270:
271: $directories = explode('/', dirname($destination));
272:
273: foreach ($directories as $directory) {
274: if (!$path_new) {
275: $path_new = $directory;
276: } else {
277: $path_new = $path_new . '/' . $directory;
278: }
279:
280: // To fix storage location
281: if (!is_dir($base_new . $path_new)) {
282: mkdir($base_new . $path_new, 0777);
283: }
284: }
285:
286: if (is_file($base_old . $destination) && !is_file($base_new . $destination)) {
287: copy($base_old . $destination, $base_new . $destination);
288: }
289: }
290:
291: if ($end < $total) {
292: $json['text'] = sprintf($this->language->get('text_storage_move'), $start, $end, $total);
293:
294: $json['next'] = $this->url->link('common/security.storage', '&user_token=' . $this->session->data['user_token'] . '&name=' . $name . '&path=' . $path . '&page=' . ($page + 1), true);
295: } else {
296: // Remove old directories and files
297: rsort($files);
298:
299: foreach ($files as $file) {
300: if (is_file($file)) {
301: unlink($file);
302: } elseif (is_dir($file)) {
303: rmdir($file);
304: }
305: }
306:
307: rmdir($base_old);
308:
309: // Modify the config files
310: $files = [
311: DIR_APPLICATION . 'config.php',
312: DIR_OPENCART . 'config.php'
313: ];
314:
315: foreach ($files as $file) {
316: $output = '';
317:
318: $lines = file($file);
319:
320: foreach ($lines as $line_id => $line) {
321: if (strpos($line, 'define(\'DIR_STORAGE') !== false) {
322: $output .= 'define(\'DIR_STORAGE\', \'' . $base_new . '\');' . "\n";
323: } else {
324: $output .= $line;
325: }
326: }
327:
328: $file = fopen($file, 'w');
329:
330: fwrite($file, $output);
331:
332: fclose($file);
333: }
334:
335: $json['success'] = $this->language->get('text_storage_success');
336: }
337: }
338:
339: $this->response->addHeader('Content-Type: application/json');
340: $this->response->setOutput(json_encode($json));
341: }
342:
343: /**
344: * Admin
345: *
346: * @return void
347: */
348: public function admin(): void {
349: $this->load->language('common/security');
350:
351: $json = [];
352:
353: if (isset($this->request->get['page'])) {
354: $page = (int)$this->request->get['page'];
355: } else {
356: $page = 1;
357: }
358:
359: if (isset($this->request->get['name'])) {
360: $name = preg_replace('[^a-zA-Z0-9]', '', basename(html_entity_decode(trim((string)$this->request->get['name']), ENT_QUOTES, 'UTF-8')));
361: } else {
362: $name = 'admin';
363: }
364:
365: if (!$this->user->hasPermission('modify', 'common/security')) {
366: $json['error'] = $this->language->get('error_permission');
367: }
368:
369: if (!$json) {
370: $base_old = DIR_OPENCART . 'admin/';
371: $base_new = DIR_OPENCART . $name . '/';
372:
373: if (!is_dir($base_old)) {
374: $json['error'] = $this->language->get('error_admin');
375: }
376:
377: if ($page == 1 && is_dir($base_new)) {
378: $json['error'] = $this->language->get('error_admin_exists');
379: }
380:
381: $blocked = [
382: 'admin',
383: 'catalog',
384: 'extension',
385: 'image',
386: 'install',
387: 'system'
388: ];
389:
390: if (in_array($name, $blocked)) {
391: $json['error'] = sprintf($this->language->get('error_admin_allowed'), $name);
392: }
393:
394: if (!is_writable(DIR_OPENCART . 'config.php') || !is_writable(DIR_APPLICATION . 'config.php')) {
395: $json['error'] = $this->language->get('error_writable');
396: }
397: }
398:
399: if (!$json) {
400: // 1. // 1. We need to copy the files, as rename cannot be used on any directory, the executing script is running under
401: $files = [];
402:
403: // Make path into an array
404: $directory = [$base_old];
405:
406: // While the path array is still populated keep looping through
407: while (count($directory) != 0) {
408: $next = array_shift($directory);
409:
410: foreach (glob(rtrim($next, '/') . '/{*,.[!.]*,..?*}', GLOB_BRACE) as $file) {
411: // If directory add to path array
412: if (is_dir($file)) {
413: $directory[] = $file;
414: }
415:
416: // Add the file to the files to be deleted array
417: $files[] = $file;
418: }
419: }
420:
421: // 2. Create the new admin folder name
422: if (!is_dir($base_new)) {
423: mkdir($base_new, 0777);
424: }
425:
426: // 3. split the file copies into chunks.
427: $total = count($files);
428: $limit = 200;
429:
430: $start = ($page - 1) * $limit;
431: $end = ($start > ($total - $limit)) ? $total : ($start + $limit);
432:
433: // 4. Copy the files across
434: foreach (array_slice($files, $start, $end) as $file) {
435: $destination = substr($file, strlen($base_old));
436:
437: // Must not have a path before files and directories can be moved
438: $path_new = '';
439:
440: $directories = explode('/', dirname($destination));
441:
442: foreach ($directories as $directory) {
443: if (!$path_new) {
444: $path_new = $directory;
445: } else {
446: $path_new = $path_new . '/' . $directory;
447: }
448:
449: if (!is_dir($base_new . $path_new)) {
450: mkdir($base_new . $path_new, 0777);
451: }
452: }
453:
454: if (is_file($base_old . $destination) && !is_file($base_new . $destination)) {
455: copy($base_old . $destination, $base_new . $destination);
456: }
457: }
458:
459: if ($end < $total) {
460: $json['text'] = sprintf($this->language->get('text_admin_move'), $start, $end, $total);
461:
462: $json['next'] = $this->url->link('common/security.admin', '&user_token=' . $this->session->data['user_token'] . '&name=' . $name . '&page=' . ($page + 1), true);
463: } else {
464: // Update the old config files
465: $file = $base_new . 'config.php';
466:
467: $output = '';
468:
469: $lines = file($file);
470:
471: foreach ($lines as $line_id => $line) {
472: $status = true;
473:
474: if (strpos($line, 'define(\'HTTP_SERVER') !== false) {
475: $output .= 'define(\'HTTP_SERVER\', \'' . substr(HTTP_SERVER, 0, strrpos(HTTP_SERVER, '/admin/')) . '/' . $name . '/\');' . "\n";
476:
477: $status = false;
478: }
479:
480: if (strpos($line, 'define(\'DIR_APPLICATION') !== false) {
481: $output .= 'define(\'DIR_APPLICATION\', DIR_OPENCART . \'' . $name . '/\');' . "\n";
482:
483: $status = false;
484: }
485:
486: if ($status) {
487: $output .= $line;
488: }
489: }
490:
491: $file = fopen($file, 'w');
492:
493: fwrite($file, $output);
494:
495: fclose($file);
496:
497: $this->session->data['success'] = $this->language->get('text_admin_success');
498:
499: // 6. redirect to the new admin
500: $json['redirect'] = str_replace('&amp;', '&', substr(HTTP_SERVER, 0, -6) . $name . '/index.php?route=common/login');
501: }
502: }
503:
504: $this->response->addHeader('Content-Type: application/json');
505: $this->response->setOutput(json_encode($json));
506: }
507:
508: /**
509: * Delete
510: *
511: * @return void
512: */
513: public function delete(): void {
514: $this->load->language('common/security');
515:
516: $json = [];
517:
518: if (isset($this->request->get['remove'])) {
519: $remove = (string)$this->request->get['remove'];
520: } else {
521: $remove = '';
522: }
523:
524: if (!$this->user->hasPermission('modify', 'common/security')) {
525: $json['error'] = $this->language->get('error_permission');
526: }
527:
528: if (!$json) {
529: $path = '';
530:
531: if ($remove == 'storage') {
532: // Storage directory exists
533: $path = DIR_SYSTEM . 'storage/';
534:
535: if (!is_dir($path) || DIR_STORAGE == $path) {
536: $json['error'] = $this->language->get('error_storage');
537: }
538: }
539:
540: // Admin directory exists
541: if ($remove == 'admin') {
542: $path = DIR_OPENCART . 'admin/';
543:
544: if (!is_dir($path) || DIR_APPLICATION == $path) {
545: $json['error'] = $this->language->get('error_admin');
546: }
547: }
548:
549: if (!$path) {
550: $json['error'] = $this->language->get('error_remove');
551: }
552: }
553:
554: if (!$json) {
555: // Delete old admin directory
556: $directory = [$path];
557:
558: // Remove paths
559: foreach ($directory as $path) {
560: $files = [];
561:
562: // While the path array is still populated keep looping through
563: while (count($directory) != 0) {
564: $next = array_shift($directory);
565:
566: if (is_dir($next)) {
567: foreach (glob(rtrim($next, '/') . '/{*,.[!.]*,..?*}', GLOB_BRACE) as $file) {
568: // If directory add to path array
569: if (is_dir($file)) {
570: $directory[] = $file;
571: }
572:
573: // Add the file to the files to be deleted array
574: $files[] = $file;
575: }
576: }
577: }
578:
579: rsort($files);
580:
581: foreach ($files as $file) {
582: if (is_file($file)) {
583: unlink($file);
584: } elseif (is_dir($file)) {
585: rmdir($file);
586: }
587: }
588:
589: rmdir($path);
590: }
591:
592: $json['success'] = $this->language->get('text_' . $remove . '_delete_success');
593: }
594:
595: $this->response->addHeader('Content-Type: application/json');
596: $this->response->setOutput(json_encode($json));
597: }
598: }
599: