1: <?php
2: namespace Opencart\Admin\Controller\Marketplace;
3: /**
4: * Class Modification
5: *
6: * @package Opencart\Admin\Controller\Marketplace
7: */
8: class Modification extends \Opencart\System\Engine\Controller {
9: /**
10: * Index
11: *
12: * @return void
13: */
14: public function index(): void {
15: $this->load->language('marketplace/modification');
16:
17: $this->document->setTitle($this->language->get('heading_title'));
18:
19: $url = '';
20:
21: if (isset($this->request->get['sort'])) {
22: $url .= '&sort=' . $this->request->get['sort'];
23: }
24:
25: if (isset($this->request->get['order'])) {
26: $url .= '&order=' . $this->request->get['order'];
27: }
28:
29: if (isset($this->request->get['page'])) {
30: $url .= '&page=' . $this->request->get['page'];
31: }
32:
33: $data['breadcrumbs'] = [];
34:
35: $data['breadcrumbs'][] = [
36: 'text' => $this->language->get('text_home'),
37: 'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'])
38: ];
39:
40: $data['breadcrumbs'][] = [
41: 'text' => $this->language->get('heading_title'),
42: 'href' => $this->url->link('marketplace/modification', 'user_token=' . $this->session->data['user_token'] . $url)
43: ];
44:
45: $data['delete'] = $this->url->link('marketplace/modification.delete', 'user_token=' . $this->session->data['user_token']);
46: $data['download'] = $this->url->link('tool/log.download', 'user_token=' . $this->session->data['user_token'] . '&filename=ocmod.log');
47: $data['upload'] = $this->url->link('tool/installer.upload', 'user_token=' . $this->session->data['user_token']);
48:
49: $data['list'] = $this->getList();
50:
51: // Log
52: $data['log'] = $this->getLog();
53:
54: $data['user_token'] = $this->session->data['user_token'];
55:
56: $data['header'] = $this->load->controller('common/header');
57: $data['column_left'] = $this->load->controller('common/column_left');
58: $data['footer'] = $this->load->controller('common/footer');
59:
60: $this->response->setOutput($this->load->view('marketplace/modification', $data));
61: }
62:
63: /**
64: * List
65: *
66: * @return void
67: */
68: public function list(): void {
69: $this->load->language('marketplace/modification');
70:
71: $this->response->setOutput($this->getList());
72: }
73:
74: /**
75: * Get List
76: *
77: * @return string
78: */
79: public function getList(): string {
80: if (isset($this->request->get['sort'])) {
81: $sort = (string)$this->request->get['sort'];
82: } else {
83: $sort = 'name';
84: }
85:
86: if (isset($this->request->get['order'])) {
87: $order = (string)$this->request->get['order'];
88: } else {
89: $order = 'ASC';
90: }
91:
92: if (isset($this->request->get['page'])) {
93: $page = (int)$this->request->get['page'];
94: } else {
95: $page = 1;
96: }
97:
98: $url = '';
99:
100: if (isset($this->request->get['sort'])) {
101: $url .= '&sort=' . $this->request->get['sort'];
102: }
103:
104: if (isset($this->request->get['order'])) {
105: $url .= '&order=' . $this->request->get['order'];
106: }
107:
108: if (isset($this->request->get['page'])) {
109: $url .= '&page=' . $this->request->get['page'];
110: }
111:
112: $data['action'] = $this->url->link('marketplace/modification.list', 'user_token=' . $this->session->data['user_token'] . $url);
113:
114: $data['modifications'] = [];
115:
116: $filter_data = [
117: 'sort' => $sort,
118: 'order' => $order,
119: 'start' => ($page - 1) * $this->config->get('config_pagination_admin'),
120: 'limit' => $this->config->get('config_pagination_admin')
121: ];
122:
123: $this->load->model('setting/modification');
124:
125: $results = $this->model_setting_modification->getModifications($filter_data);
126:
127: foreach ($results as $result) {
128: $data['modifications'][] = [
129: 'modification_id' => $result['modification_id'],
130: 'name' => $result['name'],
131: 'code' => $result['code'],
132: 'description' => $result['description'],
133: 'author' => $result['author'],
134: 'version' => $result['version'],
135: 'xml' => $result['xml'],
136: 'status' => $result['status'],
137: 'date_added' => date($this->language->get('date_format_short'), strtotime($result['date_added'])),
138: 'link' => $result['link'],
139: 'enable' => $this->url->link('marketplace/modification.enable', 'user_token=' . $this->session->data['user_token'] . '&modification_id=' . $result['modification_id']),
140: 'disable' => $this->url->link('marketplace/modification.disable', 'user_token=' . $this->session->data['user_token'] . '&modification_id=' . $result['modification_id'])
141: ];
142: }
143:
144: $url = '';
145:
146: if ($order == 'ASC') {
147: $url .= '&order=DESC';
148: } else {
149: $url .= '&order=ASC';
150: }
151:
152: $data['sort_name'] = $this->url->link('marketplace/modification', 'user_token=' . $this->session->data['user_token'] . '&sort=name' . $url, true);
153: $data['sort_author'] = $this->url->link('marketplace/modification', 'user_token=' . $this->session->data['user_token'] . '&sort=author' . $url, true);
154: $data['sort_version'] = $this->url->link('marketplace/modification', 'user_token=' . $this->session->data['user_token'] . '&sort=version' . $url, true);
155: $data['sort_date_added'] = $this->url->link('marketplace/modification', 'user_token=' . $this->session->data['user_token'] . '&sort=date_added' . $url, true);
156:
157: $url = '';
158:
159: if (isset($this->request->get['sort'])) {
160: $url .= '&sort=' . $this->request->get['sort'];
161: }
162:
163: if (isset($this->request->get['order'])) {
164: $url .= '&order=' . $this->request->get['order'];
165: }
166:
167: $modification_total = $this->model_setting_modification->getTotalModifications();
168:
169: $data['pagination'] = $this->load->controller('common/pagination', [
170: 'total' => $modification_total,
171: 'page' => $page,
172: 'limit' => $this->config->get('config_pagination_admin'),
173: 'url' => $this->url->link('marketplace/modification.list', 'user_token=' . $this->session->data['user_token'] . $url . '&page={page}')
174: ]);
175:
176: $data['results'] = sprintf($this->language->get('text_pagination'), ($modification_total) ? (($page - 1) * $this->config->get('config_pagination_admin')) + 1 : 0, ((($page - 1) * $this->config->get('config_pagination_admin')) > ($modification_total - $this->config->get('config_pagination_admin'))) ? $modification_total : ((($page - 1) * $this->config->get('config_pagination_admin')) + $this->config->get('config_pagination_admin')), $modification_total, ceil($modification_total / $this->config->get('config_pagination_admin')));
177:
178: $data['sort'] = $sort;
179: $data['order'] = $order;
180:
181: return $this->load->view('marketplace/modification_list', $data);
182: }
183:
184: /**
185: * Refresh
186: *
187: * @throws \Exception
188: *
189: * @return void
190: */
191: public function refresh(): void {
192: $this->load->language('marketplace/modification');
193:
194: $json = [];
195:
196: if (!$this->user->hasPermission('modify', 'marketplace/modification')) {
197: $json['error'] = $this->language->get('error_permission');
198: }
199:
200: if (!$json) {
201: // Just before files are deleted, if config settings say maintenance mode is off then turn it on
202: $maintenance = $this->config->get('config_maintenance');
203:
204: $this->load->model('setting/setting');
205:
206: $this->model_setting_setting->editValue('config', 'config_maintenance', '1');
207:
208: // Clear all modification files
209: $files = [];
210:
211: // Make path into an array
212: $path = [DIR_EXTENSION . 'ocmod/*'];
213:
214: // While the path array is still populated keep looping through
215: while (count($path) != 0) {
216: $next = array_shift($path);
217:
218: foreach (glob($next) as $file) {
219: // If directory add to path array
220: if (is_dir($file)) {
221: $path[] = $file . '/*';
222: }
223:
224: // Add the file to the files to be deleted array
225: $files[] = $file;
226: }
227: }
228:
229: // Reverse sort the file array
230: rsort($files);
231:
232: // Clear all modification files
233: foreach ($files as $file) {
234: if ($file != DIR_EXTENSION . 'ocmod/index.html') {
235: // If file just delete
236: if (is_file($file)) {
237: unlink($file);
238:
239: // If directory use the remove directory function
240: } elseif (is_dir($file)) {
241: rmdir($file);
242: }
243: }
244: }
245:
246: // Begin
247: $xml = [];
248:
249: // This is purely so developers they can run mods directly and have them run without upload after each change.
250: $files = glob(DIR_SYSTEM . '*.ocmod.xml');
251:
252: if ($files) {
253: foreach ($files as $file) {
254: $xml[] = file_get_contents($file);
255: }
256: }
257:
258: $this->load->model('setting/modification');
259:
260: $results = $this->model_setting_modification->getModifications();
261:
262: foreach ($results as $result) {
263: if ($result['status']) {
264: $xml[] = $result['xml'];
265: }
266: }
267:
268: // Log
269: $log = [];
270:
271: $original = [];
272: $modification = [];
273:
274: foreach ($xml as $xml) {
275: if (empty($xml)) {
276: continue;
277: }
278:
279: $dom = new \DOMDocument('1.0', 'UTF-8');
280: $dom->preserveWhiteSpace = false;
281: $dom->loadXml($xml);
282:
283: // Log
284: $log[] = 'MOD: ' . $dom->getElementsByTagName('name')->item(0)->textContent;
285:
286: // Store a backup recovery of the modification code in case we need to use it if an abort attribute is used.
287: $recovery = $modification;
288:
289: $files = $dom->getElementsByTagName('modification')->item(0)->getElementsByTagName('file');
290:
291: foreach ($files as $file) {
292: $operations = $file->getElementsByTagName('operation');
293:
294: $files = explode('|', str_replace("\\", '/', $file->getAttribute('path')));
295:
296: foreach ($files as $file) {
297: $path = '';
298:
299: // Get the full path of the files that are going to be used for modification
300: if ((substr($file, 0, 7) == 'catalog')) {
301: $path = DIR_CATALOG . substr($file, 8);
302: }
303:
304: if ((substr($file, 0, 5) == 'admin')) {
305: $path = DIR_APPLICATION . substr($file, 6);
306: }
307:
308: if ((substr($file, 0, 9) == 'extension')) {
309: $path = DIR_EXTENSION . substr($file, 9);
310: }
311:
312: if ((substr($file, 0, 6) == 'system')) {
313: $path = DIR_SYSTEM . substr($file, 7);
314: }
315:
316: if ($path) {
317: $files = glob($path, GLOB_BRACE);
318:
319: if ($files) {
320: foreach ($files as $file) {
321: if (substr($file, 0, strlen(DIR_APPLICATION)) == DIR_APPLICATION) {
322: $key = 'admin/' . substr($file, strlen(DIR_APPLICATION));
323: }
324:
325: // Get the key to be used for the modification cache filename.
326: if (substr($file, 0, strlen(DIR_CATALOG)) == DIR_CATALOG) {
327: $key = 'catalog/' . substr($file, strlen(DIR_CATALOG));
328: }
329:
330: if (substr($file, 0, strlen(DIR_EXTENSION)) == DIR_EXTENSION) {
331: $key = 'extension/' . substr($file, strlen(DIR_EXTENSION));
332: }
333:
334: if (substr($file, 0, strlen(DIR_SYSTEM)) == DIR_SYSTEM) {
335: $key = 'system/' . substr($file, strlen(DIR_SYSTEM));
336: }
337:
338: // If file contents is not already in the modification array we need to load it.
339: if (!isset($modification[$key])) {
340: $content = file_get_contents($file);
341:
342: $modification[$key] = preg_replace('~\r?\n~', "\n", $content);
343: $original[$key] = preg_replace('~\r?\n~', "\n", $content);
344:
345: // Log
346: $log[] = PHP_EOL . 'FILE: ' . $key;
347:
348: } else {
349: // Log
350: $log[] = PHP_EOL . 'FILE: (sub modification) ' . $key;
351:
352: }
353:
354: foreach ($operations as $operation) {
355: $error = $operation->getAttribute('error');
356:
357: // Ignoreif
358: $ignoreif = $operation->getElementsByTagName('ignoreif')->item(0);
359:
360: if ($ignoreif) {
361: if ($ignoreif->getAttribute('regex') != 'true') {
362: if (strpos($modification[$key], $ignoreif->textContent) !== false) {
363: continue;
364: }
365: } else {
366: if (preg_match($ignoreif->textContent, $modification[$key])) {
367: continue;
368: }
369: }
370: }
371:
372: $status = false;
373:
374: // Search and replace
375: if ($operation->getElementsByTagName('search')->item(0)->getAttribute('regex') != 'true') {
376: // Search
377: $search = $operation->getElementsByTagName('search')->item(0)->textContent;
378: $trim = $operation->getElementsByTagName('search')->item(0)->getAttribute('trim');
379: $index = $operation->getElementsByTagName('search')->item(0)->getAttribute('index');
380:
381: // Trim line if no trim attribute is set or is set to true.
382: if (!$trim || $trim == 'true') {
383: $search = trim($search);
384: }
385:
386: // Add
387: $add = $operation->getElementsByTagName('add')->item(0)->textContent;
388: $trim = $operation->getElementsByTagName('add')->item(0)->getAttribute('trim');
389: $position = $operation->getElementsByTagName('add')->item(0)->getAttribute('position');
390: $offset = (int)$operation->getElementsByTagName('add')->item(0)->getAttribute('offset');
391:
392: // Trim line if is set to true.
393: if ($trim == 'true') {
394: $add = trim($add);
395: }
396:
397: // Log
398: $log[] = 'CODE: ' . $search;
399:
400: // Check if using indexes
401: if ($index !== '') {
402: $indexes = explode(',', $index);
403: } else {
404: $indexes = '';
405: }
406:
407: // Get all the matches
408: $i = 0;
409:
410: $lines = explode("\n", $modification[$key]);
411:
412: for ($line_id = 0; $line_id < count($lines); $line_id++) {
413: $line = $lines[$line_id];
414:
415: // Status
416: $match = false;
417:
418: // Check to see if the line matches the search code.
419: if (stripos($line, $search) !== false) {
420: // If indexes are not used then just set the found status to true.
421: if (!$indexes) {
422: $match = true;
423: } elseif (in_array($i, $indexes)) {
424: $match = true;
425: }
426:
427: $i++;
428: }
429:
430: // Now for replacing or adding to the matched elements
431: if ($match) {
432: switch ($position) {
433: default:
434: case 'replace':
435: $new_lines = explode("\n", $add);
436:
437: if ($offset < 0) {
438: array_splice($lines, $line_id + $offset, abs($offset) + 1, [str_replace($search, $add, $line)]);
439:
440: $line_id -= $offset;
441: } else {
442: array_splice($lines, $line_id, $offset + 1, [str_replace($search, $add, $line)]);
443: }
444: break;
445: case 'before':
446: $new_lines = explode("\n", $add);
447:
448: array_splice($lines, $line_id - $offset, 0, $new_lines);
449:
450: $line_id += count($new_lines);
451: break;
452: case 'after':
453: $new_lines = explode("\n", $add);
454:
455: array_splice($lines, ($line_id + 1) + $offset, 0, $new_lines);
456:
457: $line_id += count($new_lines);
458: break;
459: }
460:
461: // Log
462: $log[] = 'LINE: ' . $line_id;
463:
464: $status = true;
465: }
466: }
467:
468: $modification[$key] = implode("\n", $lines);
469: } else {
470: $search = trim($operation->getElementsByTagName('search')->item(0)->textContent);
471: $limit = (int)$operation->getElementsByTagName('search')->item(0)->getAttribute('limit');
472: $replace = trim($operation->getElementsByTagName('add')->item(0)->textContent);
473:
474: // Limit
475: if (!$limit) {
476: $limit = -1;
477: }
478:
479: // Log
480: $match = [];
481:
482: preg_match_all($search, $modification[$key], $match, PREG_OFFSET_CAPTURE);
483:
484: // Remove part of the result if a limit is set.
485: if ($limit > 0) {
486: $match[0] = array_slice($match[0], 0, $limit);
487: }
488:
489: if ($match[0]) {
490: $log[] = 'REGEX: ' . $search;
491:
492: for ($i = 0; $i < count($match[0]); $i++) {
493: $log[] = 'LINE: ' . (substr_count(substr($modification[$key], 0, $match[0][$i][1]), "\n") + 1);
494: }
495:
496: $status = true;
497: }
498:
499: // Make the modification
500: $modification[$key] = preg_replace($search, $replace, $modification[$key], $limit);
501: }
502:
503: if (!$status) {
504: // Abort applying this modification completely.
505: if ($error == 'abort') {
506: $modification = $recovery;
507: // Log
508: $log[] = 'NOT FOUND - ABORTING!';
509: break 4;
510: }
511: // Skip current operation or break
512: elseif ($error == 'skip') {
513: // Log
514: $log[] = 'NOT FOUND - OPERATION SKIPPED!';
515: continue;
516: }
517: // Break current operations
518: else {
519: // Log
520: $log[] = 'NOT FOUND - OPERATIONS ABORTED!';
521: break;
522: }
523: }
524: }
525: }
526: }
527: }
528: }
529: }
530:
531: // Log
532: $log[] = '----------------------------------------------------------------';
533: }
534:
535: // Log
536: $ocmod = new \Opencart\System\Library\Log('ocmod.log');
537: $ocmod->write(implode("\n", $log));
538:
539: // Write all modification files
540: foreach ($modification as $key => $value) {
541: // Only create a file if there are changes
542: if ($original[$key] != $value) {
543: $path = '';
544:
545: $directories = explode('/', dirname($key));
546:
547: foreach ($directories as $directory) {
548: $path = $path . '/' . $directory;
549:
550: if (!is_dir(DIR_EXTENSION . 'ocmod/' . $path)) {
551: @mkdir(DIR_EXTENSION . 'ocmod/' . $path, 0777);
552: }
553: }
554:
555: $handle = fopen(DIR_EXTENSION . 'ocmod/' . $key, 'w');
556:
557: fwrite($handle, $value);
558:
559: fclose($handle);
560: }
561: }
562:
563: // Maintance mode back to original settings
564: $this->model_setting_setting->editValue('config', 'config_maintenance', $maintenance);
565:
566: // Do not return success message if refresh() was called with $data
567: $json['success'] = $this->language->get('text_success');
568: }
569:
570: $this->response->addHeader('Content-Type: application/json');
571: $this->response->setOutput(json_encode($json));
572: }
573:
574: /**
575: * Log
576: *
577: * @return void
578: */
579: public function log(): void {
580: $this->response->setOutput($this->getLog());
581: }
582:
583: /**
584: * getLog
585: *
586: * @return string
587: */
588: public function getLog(): string {
589: $file = DIR_LOGS . 'ocmod.log';
590:
591: if (is_file($file)) {
592: return htmlentities(file_get_contents($file, true, null));
593: } else {
594: return '';
595: }
596: }
597:
598: /**
599: * Clear
600: *
601: * @return void
602: */
603: public function clear(): void {
604: $this->load->language('marketplace/modification');
605:
606: $json = [];
607:
608: if (!$this->user->hasPermission('modify', 'marketplace/modification')) {
609: $json['error'] = $this->language->get('error_permission');
610: }
611:
612: if (!$json) {
613: $files = [];
614:
615: // Make path into an array
616: $path = [DIR_EXTENSION . 'ocmod/*'];
617:
618: // While the path array is still populated keep looping through
619: while (count($path) != 0) {
620: $next = array_shift($path);
621:
622: foreach (glob($next) as $file) {
623: // If directory add to path array
624: if (is_dir($file)) {
625: $path[] = $file . '/*';
626: }
627:
628: // Add the file to the files to be deleted array
629: $files[] = $file;
630: }
631: }
632:
633: // Reverse sort the file array
634: rsort($files);
635:
636: // Clear all modification files
637: foreach ($files as $file) {
638: if ($file != DIR_EXTENSION . 'ocmod/index.html') {
639: // If file just delete
640: if (is_file($file)) {
641: unlink($file);
642:
643: // If directory use the remove directory function
644: } elseif (is_dir($file)) {
645: rmdir($file);
646: }
647: }
648: }
649:
650: $json['success'] = $this->language->get('text_success');
651: }
652:
653: $this->response->addHeader('Content-Type: application/json');
654: $this->response->setOutput(json_encode($json));
655: }
656:
657: /**
658: * Enable
659: *
660: * @return void
661: */
662: public function enable(): void {
663: $this->load->language('marketplace/modification');
664:
665: $json = [];
666:
667: if (isset($this->request->get['modification_id'])) {
668: $modification_id = (int)$this->request->get['modification_id'];
669: } else {
670: $modification_id = 0;
671: }
672:
673: if (!$this->user->hasPermission('modify', 'marketplace/modification')) {
674: $json['error'] = $this->language->get('error_permission');
675: }
676:
677: if (!$json) {
678: $this->load->model('setting/modification');
679:
680: $this->model_setting_modification->editStatus($modification_id, true);
681:
682: $json['success'] = $this->language->get('text_success');
683: }
684:
685: $this->response->addHeader('Content-Type: application/json');
686: $this->response->setOutput(json_encode($json));
687: }
688:
689: /**
690: * Disable
691: *
692: * @return void
693: */
694: public function disable(): void {
695: $this->load->language('marketplace/modification');
696:
697: $json = [];
698:
699: if (isset($this->request->get['modification_id'])) {
700: $modification_id = (int)$this->request->get['modification_id'];
701: } else {
702: $modification_id = 0;
703: }
704:
705: if (!$this->user->hasPermission('modify', 'marketplace/modification')) {
706: $json['error'] = $this->language->get('error_permission');
707: }
708:
709: if (!$json) {
710: $this->load->model('setting/modification');
711:
712: $this->model_setting_modification->editStatus($modification_id, false);
713:
714: $json['success'] = $this->language->get('text_success');
715: }
716:
717: $this->response->addHeader('Content-Type: application/json');
718: $this->response->setOutput(json_encode($json));
719: }
720:
721: /**
722: * Delete
723: *
724: * @return void
725: */
726: public function delete(): void {
727: $this->load->language('marketplace/modification');
728:
729: $json = [];
730:
731: if (isset($this->request->post['selected'])) {
732: $selected = (array)$this->request->post['selected'];
733: } else {
734: $selected = [];
735: }
736:
737: if (!$this->user->hasPermission('modify', 'marketplace/modification')) {
738: $json['error'] = $this->language->get('error_permission');
739: }
740:
741: if (!$json) {
742: $this->load->model('setting/modification');
743:
744: foreach ($selected as $modification_id) {
745: $this->model_setting_modification->deleteModification($modification_id);
746: }
747:
748: $json['success'] = $this->language->get('text_success');
749: }
750:
751: $this->response->addHeader('Content-Type: application/json');
752: $this->response->setOutput(json_encode($json));
753: }
754: }
755: