PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.20
VikAppointments Services Booking Calendar v1.2.20
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / libraries / mvc / admin / models / overrides.php
vikappointments / libraries / mvc / admin / models Last commit date
acl.php 1 month ago license.php 1 month ago override.php 1 month ago overrides.php 1 month ago shortcode.php 1 month ago shortcodes.php 1 month ago
overrides.php
704 lines
1 <?php
2 /**
3 * @package VikAppointments
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2024 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 JLoader::import('adapter.mvc.models.form');
15
16 /**
17 * VikAppointments plugin Overrides model.
18 * @wponly
19 *
20 * @since 1.2.13
21 * @see JModel
22 */
23 class VikAppointmentsModelOverrides extends JModel
24 {
25 /**
26 * A list containing all the back-end views to exclude.
27 *
28 * @var array
29 */
30 public $_excludedAdminViews = ['getpro', 'gotopro', 'overrides', 'updateprogram'];
31
32 /**
33 * A list containing all the front-end views to exclude.
34 *
35 * @var array
36 */
37 public $_excludedSiteViews = [];
38
39 /**
40 * A list containing all the widgets to exclude.
41 *
42 * @var array
43 */
44 public $_excludedModules = [];
45
46 /**
47 * A list containing all the layouts to exclude.
48 *
49 * @var array
50 */
51 public $_excludedLayouts = [
52 // skip alert reserved to Joomla platform
53 '\/site\/layouts\/blocks\/alert\.php$',
54 // any file that ends with Joomla (e.g. blocks/login/joomla.php)
55 'joomla\.php$',
56 // the leftboard menu created used by Joomla
57 '\/admin\/layouts\/menu\/leftboard',
58 ];
59
60 /**
61 * Checks whether the specified file supports overrides.
62 *
63 * @param mixed $tree Either the tree array or the client string.
64 * @param string $file The file to look for.
65 *
66 * @return mixed The node found on success, false otherwise.
67 */
68 public function isSupported($tree, $file)
69 {
70 if (!is_array($tree))
71 {
72 // client given, generate tree
73 $tree = $this->getTree($tree);
74 }
75
76 // look for a leaf
77 if (isset($tree['folder']) && !$tree['folder'])
78 {
79 // leaf found, look for a match with the file path
80 if ($file == $tree['path'] || $file == $tree['override'])
81 {
82 // match found, override supported
83 return $tree;
84 }
85 else
86 {
87 // match not found, go ahead
88 return false;
89 }
90 }
91
92 // scan files if we are inside a node
93 if (isset($tree['files']))
94 {
95 $tree = $tree['files'];
96 }
97
98 // iterate nodes
99 foreach ($tree as $node)
100 {
101 // recursively check whether the node contains
102 // a file matching the specified path
103 if ($leaf = $this->isSupported($node, $file))
104 {
105 // leaf found, override supported
106 return $leaf;
107 }
108 }
109
110 // file not supported
111 return false;
112 }
113
114 /**
115 * Creates the tree containing the available overrides for the specified client.
116 *
117 * @param string $client The client to look for. Supports the
118 * following options:
119 * - administrator back-end views;
120 * - site front-end views;
121 * - layouts admin, site and core layouts;
122 * - modules plugin widgets.
123 *
124 * @return array The resulting tree.
125 */
126 public function getTree($client)
127 {
128 // import file system helper
129 JLoader::import('adapter.filesystem.folder');
130
131 // look for administrator/site views
132 if (preg_match("/^(admin(?:istrator)?|site)$/i", $client))
133 {
134 // scan views overrides
135 $tree = $this->getViewsTree($client);
136 }
137 else if (preg_match("/^layouts?$/i", $client))
138 {
139 // scan layouts overrides
140 $tree = $this->getLayoutsTree();
141 }
142 else if (preg_match("/^(modules?|widgets?)$/i", $client))
143 {
144 // scan modules overrides
145 $tree = $this->getModulesTree();
146 }
147 else
148 {
149 // client not supported, throw exception
150 throw new Exception(sprintf('Override [%s] client not supported', $client), 500);
151 }
152
153 return $tree;
154 }
155
156 /**
157 * Creates the tree containing the available overrides for the admin/site views.
158 *
159 * @param string $client The client to look for.
160 *
161 * @return array The resulting tree.
162 */
163 protected function getViewsTree($client)
164 {
165 $tree = [];
166
167 // build path according to the specified client
168 if (preg_match("/^admin(?:istrator)?$/i", $client))
169 {
170 // admin path
171 $path = VAPADMIN . DIRECTORY_SEPARATOR . 'views';
172
173 // get list of excluded views
174 $excluded = $this->_excludedAdminViews;
175
176 // define client folder
177 $clientFolder = 'admin';
178 }
179 else
180 {
181 // site path
182 $path = VAPBASE . DIRECTORY_SEPARATOR . 'views';
183
184 // get list of excluded views
185 $excluded = $this->_excludedSiteViews;
186
187 // define client folder
188 $clientFolder = 'site';
189 }
190
191 // retrieve temporary uploads path
192 $upload = wp_upload_dir();
193
194 // create base path of overrides
195 $overridesPath = JPath::clean($upload['basedir'] . '/vikappointments/overrides/' . $clientFolder . '/');
196
197 // get all folders at the specified path
198 $views = JFolder::folders($path, '.', $recursive = false, $fullPath = true);
199
200 // check whether the plugin supports specific views only for the WordPress platform
201 if (JFolder::exists(VIKAPPOINTMENTS_LIBRARIES . '/mvc/' . $clientFolder . '/views'))
202 {
203 $views = array_merge($views, JFolder::folders(VIKAPPOINTMENTS_LIBRARIES . '/mvc/' . $clientFolder . '/views', '.', $recursive = false, $fullPath = true));
204 }
205
206 // iterate views and extract all the supported templates
207 foreach ($views as $view)
208 {
209 $viewName = basename($view);
210
211 // make sure the view should not be excluded
212 if (!$this->isExcluded($viewName, $excluded))
213 {
214 // create node to inject within the tree
215 $node = [
216 'name' => $viewName,
217 'path' => $view,
218 'folder' => true,
219 'files' => [],
220 ];
221
222 // build path containing the view templates
223 $tmpl = JPath::clean($view . '/tmpl');
224
225 // scan files within the view
226 $files = JFolder::files($tmpl, '\.php$', $recursive = false, $fullPath = true);
227
228 // iterate files
229 foreach ($files as $file)
230 {
231 $filename = basename($file);
232
233 // create override path
234 $fileOverridePath = $overridesPath . $viewName . DIRECTORY_SEPARATOR . $filename;
235
236 $published = false;
237
238 // look for an existing override
239 if (JFile::exists($fileOverridePath))
240 {
241 // the file owns an override
242 $override = $published = true;
243 }
244 else
245 {
246 // look for an unpublished override
247 $unpublishedPath = $overridesPath . $viewName . DIRECTORY_SEPARATOR . '__' . $filename;
248
249 // check whether the file exists
250 if (JFile::exists($unpublishedPath))
251 {
252 // the file owns an unpublished override
253 $override = true;
254 }
255 else
256 {
257 // missing override
258 $override = false;
259 }
260 }
261
262 // register node within the parent
263 $node['files'][] = [
264 'name' => $filename,
265 'path' => $file,
266 'override' => $fileOverridePath,
267 'folder' => false,
268 'has' => $override,
269 'published' => $published,
270 ];
271 }
272
273 // skip the folder in case it does not contain any files
274 if (!$node['files'])
275 {
276 continue;
277 }
278
279 // append node to the tree
280 $tree[] = $node;
281 }
282 }
283
284 // sort folders by ascending name
285 usort($tree, function($a, $b)
286 {
287 return strcasecmp($a['name'], $b['name']);
288 });
289
290 return $tree;
291 }
292
293 /**
294 * Creates the tree containing the available overrides for the layouts.
295 * The first level will always contain these nodes: admin, site, core.
296 *
297 * @return array The resulting tree.
298 */
299 protected function getLayoutsTree()
300 {
301 $tree = [];
302
303 // administrator layouts
304 $tree[] = [
305 'name' => 'admin',
306 'path' => VAPADMIN . DIRECTORY_SEPARATOR . 'layouts',
307 'folder' => true,
308 'files' => [],
309 ];
310
311 // site layouts
312 $tree[] = [
313 'name' => 'site',
314 'path' => VAPBASE . DIRECTORY_SEPARATOR . 'layouts',
315 'folder' => true,
316 'files' => [],
317 ];
318
319 // system layouts
320 $tree[] = [
321 'name' => 'core',
322 'path' => VIKAPPOINTMENTS_LIBRARIES . DIRECTORY_SEPARATOR . 'html',
323 'folder' => true,
324 'files' => [],
325 ];
326
327 // retrieve temporary uploads path
328 $upload = wp_upload_dir();
329
330 // create base path of overrides
331 $overridesPath = JPath::clean($upload['basedir'] . '/vikappointments/layouts/');
332
333 // iterate main nodes
334 foreach ($tree as &$node)
335 {
336 if ($node['name'] == 'site')
337 {
338 $op = $overridesPath . 'site' . DIRECTORY_SEPARATOR;
339 }
340 else
341 {
342 $op = $overridesPath . 'admin' . DIRECTORY_SEPARATOR;
343
344 if ($node['name'] == 'core')
345 {
346 // append HTML folder in case of core client
347 $op .= 'html' . DIRECTORY_SEPARATOR;
348 }
349 }
350
351 // scan and construct node recursively
352 $this->_scanNode($node, $op);
353 }
354
355 return $tree;
356 }
357
358 /**
359 * Creates the tree containing the available overrides for the plugin widgets.
360 *
361 * @return array The resulting tree.
362 */
363 protected function getModulesTree()
364 {
365 $tree = [];
366
367 // retrieve temporary uploads path
368 $upload = wp_upload_dir();
369
370 // create base path of overrides
371 $overridesPath = JPath::clean($upload['basedir'] . '/vikappointments/overrides/modules/');
372
373 // get all folders at the specified path
374 $modules = JFolder::folders(VAPMODULES, '.', $recursive = false, $fullPath = true);
375
376 // iterate modules and extract all the supported templates
377 foreach ($modules as $module)
378 {
379 $modName = basename($module);
380
381 // make sure the module should not be excluded
382 if (!$this->isExcluded($modName, $this->_excludedModules))
383 {
384 // create node to inject within the tree
385 $node = [
386 'name' => preg_replace("/^mod_vikappointments_/i", '', $modName),
387 'path' => $module,
388 'folder' => true,
389 'files' => [],
390 ];
391
392 // build path containing the module templates
393 $tmpl = JPath::clean($module . '/tmpl');
394
395 // scan files within the module
396 $files = JFolder::files($tmpl, '\.php$', $recursive = false, $fullPath = true);
397
398 if (!$files)
399 {
400 /**
401 * Do not include module within the tree in case there are not overridable files.
402 *
403 * @since 1.3
404 */
405 continue;
406 }
407
408 // iterate files
409 foreach ($files as $file)
410 {
411 $filename = basename($file);
412
413 // create override path
414 $fileOverridePath = $overridesPath . $modName . DIRECTORY_SEPARATOR . $filename;
415
416 $published = false;
417
418 // look for an existing override
419 if (JFile::exists($fileOverridePath))
420 {
421 // the file owns an override
422 $override = $published = true;
423 }
424 else
425 {
426 // look for an unpublished override
427 $unpublishedPath = $overridesPath . $modName . DIRECTORY_SEPARATOR . '__' . $filename;
428
429 // check whether the file exists
430 if (JFile::exists($unpublishedPath))
431 {
432 // the file owns an unpublished override
433 $override = true;
434 }
435 else
436 {
437 // missing override
438 $override = false;
439 }
440 }
441
442 // register node within the parent
443 $node['files'][] = [
444 'name' => $filename,
445 'path' => $file,
446 'override' => $fileOverridePath,
447 'folder' => false,
448 'has' => $override,
449 'published' => $published,
450 ];
451 }
452
453 // append node to the tree
454 $tree[] = $node;
455 }
456 }
457
458 return $tree;
459 }
460
461 /**
462 * Recursive function used to scan the folder and files within the specified node.
463 *
464 * @param array &$node The node to scan.
465 * @param string $base The base path in which the overrides should locate.
466 *
467 * @return void
468 */
469 protected function _scanNode(&$node, $base)
470 {
471 // get all folders at the node path
472 $folders = JFolder::folders($node['path'], '.', $recursive = false, $fullPath = true);
473
474 // get all files at the node path
475 $files = JFolder::files($node['path'], '\.php$', $recursive = false, $fullPath = true);
476
477 // iterate folders
478 foreach ($folders as $folder)
479 {
480 // make sure the folder should not be excluded
481 if (!$this->isExcluded($folder, $this->_excludedLayouts))
482 {
483 $folderName = basename($folder);
484
485 // create folder node
486 $tmp = [
487 'name' => basename($folder),
488 'path' => $folder,
489 'folder' => true,
490 'files' => [],
491 ];
492
493 // folder recursive scan
494 $this->_scanNode($tmp, $base . $folderName . DIRECTORY_SEPARATOR);
495
496 // append folder to main node
497 $node['files'][] = $tmp;
498 }
499 }
500
501 // iterate files
502 foreach ($files as $file)
503 {
504 // make sure the file should not be excluded
505 if (!$this->isExcluded($file, $this->_excludedLayouts))
506 {
507 $filename = basename($file);
508
509 // create override path
510 $fileOverridePath = $base . $filename;
511
512 $published = false;
513
514 // look for an existing override
515 if (JFile::exists($fileOverridePath))
516 {
517 // the file owns an override
518 $override = $published = true;
519 }
520 else
521 {
522 // look for an unpublished override
523 $unpublishedPath = $base . DIRECTORY_SEPARATOR . '__' . $filename;
524
525 // check whether the file exists
526 if (JFile::exists($unpublishedPath))
527 {
528 // the file owns an unpublished override
529 $override = true;
530 }
531 else
532 {
533 // missing override
534 $override = false;
535 }
536 }
537
538 // create file node
539 $tmp = [
540 'name' => $filename,
541 'path' => $file,
542 'override' => $fileOverridePath,
543 'folder' => false,
544 'has' => $override,
545 'published' => $published,
546 ];
547
548 // append folder to main node
549 $node['files'][] = $tmp;
550 }
551 }
552 }
553
554 /**
555 * Check whether the specified path matches one of
556 * the specified regex.
557 *
558 * @param string $path The path to look for.
559 * @param array $pool An array of regex.
560 *
561 * @return boolean True if excluded, false otherwise.
562 */
563 protected function isExcluded($path, $pool)
564 {
565 $path = preg_replace("/[\\\\]/", '/', $path);
566
567 // iterate pool
568 foreach ($pool as $match)
569 {
570 // exec regex
571 if (preg_match("/$match/i", $path))
572 {
573 // inside the pool, exclude file
574 return true;
575 }
576 }
577
578 // can include file
579 return false;
580 }
581
582 /**
583 * Returns a lookup containing all the existing (and published) overrides,
584 * categorized by override group (admin, site, layouts, modules).
585 *
586 * Only the files that are an actual override of the core files will be taken here.
587 * In example, default_foo_bar.php is not part of VikAppointments and therefore it
588 * will be discarded.
589 *
590 * @return array
591 */
592 public function getAllOverrides()
593 {
594 // retrieve temporary uploads path
595 $upload = wp_upload_dir();
596
597 // fetch all the files created on each section
598 $lookup = [
599 'admin' => JFolder::files($upload['basedir'] . '/vikappointments/overrides/admin/', '\.php$', true, true),
600 'site' => JFolder::files($upload['basedir'] . '/vikappointments/overrides/site/', '\.php$', true, true),
601 'layouts' => JFolder::files($upload['basedir'] . '/vikappointments/layouts/', '\.php$', true, true),
602 'modules' => JFolder::files($upload['basedir'] . '/vikappointments/overrides/modules/', '\.php$', true, true),
603 ];
604
605 // exclude all the overrides that are actually unpublished
606 foreach ($lookup as &$files)
607 {
608 if (!is_array($files))
609 {
610 $files = [];
611 }
612
613 $files = array_values(array_filter($files, function($file)
614 {
615 // the override file name must not start with 2 underscores
616 return strpos(basename($file), '__') !== 0;
617 }));
618 }
619
620 // get rid of the sections without files
621 return array_filter($lookup);
622 }
623
624 /**
625 * Calculates the differences among the provided override and the original file.
626 *
627 * @param string $override The override buffer.
628 * @param string $original The original buffer.
629 * @param array $options A configuration array:
630 * - use_spaces bool Whether the tabs should be converted into 4-spaces (true by default);
631 * - lines_before int The number of lines to display before a detected difference (2 by default);
632 * - lines_after int The number of lines to display after a detected difference (2 by default).
633 *
634 * @return array An array holding the detected differences. The key of the array indicates the real line of the file.
635 * The values of the list are arrays containing the content of the file (index 0) and the detected difference:
636 * -1 for delete, 0 for keep and 1 for insert.
637 */
638 public function calcDiff(string $override, string $original, $options = [])
639 {
640 if ($options['use_spaces'] ?? true)
641 {
642 // convert tabs into 4 spaces
643 $override = preg_replace("/\t/", ' ', $override);
644 $original = preg_replace("/\t/", ' ', $original);
645 }
646
647 if ($override === $original)
648 {
649 // no differences we don't need to proceed
650 return [];
651 }
652
653 // the number of lines to display before and after the detected differences
654 $options['lines_before'] = abs((int) ($options['lines_before'] ?? 2));
655 $options['lines_after'] = abs((int) ($options['lines_after'] ?? 2));
656
657 // convert file buffers into an array of lines
658 $override = preg_split("/\R/", $override);
659 $original = preg_split("/\R/", $original);
660
661 // calculate differences
662 $lines = (new Fisharebest\Algorithm\MyersDiff)->calculate($original, $override);
663
664 // dynamically decreases the lines count depending on the removed lines
665 $pad = 0;
666
667 $copy = [];
668
669 foreach ($lines as $index => $line)
670 {
671 // register line number on array position 2
672 $line[2] = $index + 1 + $pad;
673 $lines[$index] = $line;
674
675 // increase line pad by -1 (only if deleted, 0 otherwise)
676 $pad += min(0, $line[1]);
677 }
678
679 $diff = [];
680
681 // preserve only the differences and a few lines before and after
682 foreach ($lines as $index => $line)
683 {
684 if ($line[1] === Fisharebest\Algorithm\MyersDiff::KEEP)
685 {
686 // same line, skip
687 continue;
688 }
689
690 // difference detected, replicate N lines before and M lines after
691 for ($j = $index - $options['lines_before']; $j <= $index + $options['lines_after']; $j++)
692 {
693 // make sure we are not going out of bounds
694 if (isset($lines[$j]))
695 {
696 $diff[$j] = $lines[$j];
697 }
698 }
699 }
700
701 return $diff;
702 }
703 }
704