forked from MetaTunes/ProcessDbMigrate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathProcessDbMigrate.module.php
3827 lines (3623 loc) · 153 KB
/
ProcessDbMigrate.module.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php namespace ProcessWire;
/**
* ProcessWire DbMigrate
* by Mark Evens
* with tips and snippets from Adrian Jones, Bernhard Baumrock and Robin Sallis
*
* Class ProcessDbMigrate
*
* A module to manage migrations through the PW GUI
*
* @package ProcessWire
*
* INFO-DERIVED PROPERTIES
* ==================
* @property string $name
* @property string $parent Path to parent
* @property string $parentUrl Url to parent
* @property string $parentHttpUrl Full HttpUrl to parent
* @property string $title
* @property string $adminProcess
*
* MODULE CONFIGURATION PROPERTIES
* ===============================
* @property string $enable_dbMigrate Enable the module (not checking this removes almost all the functionality and does not show page contents for migrations). Default is checked
* @property string $help The help md
* @property string $database_name User-assigned name of the current database
* @property string $suppress_hooks Suppress hook-related functionality (efectively disable module to speed site)
* @property boolean $show_name Show database name as message on all admin
* @property string $exclude_fieldtypes Field types to always exclude from migrations
* @property string $exclude_fieldnames Field names to always exclude from migrations
* @property string $exclude_attributes Object attributes to always exclude from migrations
* @property boolean $auto_install Disable auto-install of bootstrap on upgrade. Default is checked.
* @property boolean $prevent_overlap Prevent page changes where the page is within the scope of an unlocked installable migration. Default is checked.
* @property integer $install_repeats Number of times to repeat the installation process (if necessary). Default is 3.
*
* OTHER SETTING PROPERTIES
* ================================
* @property object $migrations The parent page for migration pages
* @property object $comparisons The parent page for comparison pages
* @property object $migrationTemplate The template for migration pages
* @property object $comparisonTemplate The template for comparison pages
* @property object $migrationsTemplate The template for the parent page
* @property string $migrationsPath Path to the directory holding the migrations .json files
* @property string $comparisonsPath Path to the directory holding the comparisons .json files
* @property string $modulePath Path to this module
* @property string $bootstrapPath Path to the directory holding the original bootstrap data (it is copied to migrationsPath on installation)
* @property DbMigrationPage $bootstrap
* @property string $adminPath Path to the admin root (page id = 2)
* @property string $adminUrl Url to admin root
* @property string $adminHttpUrl Full url to admin root
* @property object $trackingMigration The migration page with 'log changes' set (if any) - should be only one such page as hooks prevent more than one (otherwise first used)
* @property object $trackingField The object currently being tracked by 'log changes' (if a field)
*
*
* TEMP PROPERTIES
* ================
* @property boolean $first
*
* HOOKABLE METHODS
* =================
* @method array|string execute() Display setup page
* @method string executeDatabaseComparison() Display database comparison setup page
* @method void executeGetComparisons() Refresh comparisons
* @method void executeGetMigrations() Refresh comparisons
* @method void exportData($migrationPage) Export json from migration definition or compare with json
* @method void install($upgrade = false) Install or upgrade the module
* @method void installMigration($migrationPage) Install the specified migration
* @method void lockMigration($migrationPage, $migrationFolder) Lock the migration
* @method void newPage($template, $parent, $title, $values) Create new migration or comparison page
* @method string previewDiffs($migrationPage, $comparisonType, $button) Preview migration differences
* @method void removeFiles($migrationPage, $oldOnly = false) Remove json files for migration
* @method void uninstallMigration($migrationPage) Uninstall (roll back) the specified migration
* @method void unlockMigration($migrationPage, $migrationFolder) Unlock the migration
* @method void upgrade($fromVersion, $toVersion) Upgrade the module
*
*/
class ProcessDbMigrate extends Process implements Module, ConfigurableModule {
/**
* Although getModuleInfo has been replaced by the separate info file, we need access to the info in this module, hence this function
*
* @return mixed
*/
public static function moduleInfo() {
require('ProcessDbMigrate.info.php');
/* @var $info array */ //$info is defined in ProcessDbMigrate.info.php
//$this->bd($info, 'module info');
return $info;
}
const debug = false;
/*
* Name of template for migration pages
*
*/
const MIGRATION_TEMPLATE = 'DbMigration';
/*
* Name of template for comparison pages
*
*/
const COMPARISON_TEMPLATE = 'DbComparison';
/*
* Name of template for parent to migration and comparison pages
*
*/
const MIGRATION_PARENT_TEMPLATE = 'DbMigrations';
/*
* Name of parent page for migration pages
*
*/
const MIGRATION_PARENT = 'dbmigrations/';
/*
* Name of parent page for comparison pages
*
*/
const COMPARISON_PARENT = 'dbcomparisons/';
/*
* (Partial) path to migrations
*
*/
const MIGRATION_PATH = 'DbMigrate/migrations/';
/*
* (Partial) path to comparisons
*
*/
const COMPARISON_PATH = 'DbMigrate/comparisons/';
/*
* Prefix to use for migrations created from comparisons
*
*/
const XCPREFIX = 'xc-';
/*
* The admin path of the source used to create the bootstrap json
*
*/
const SOURCE_ADMIN = '/processwire/';
/*
* Field types to always ignore in migrations
*
*/
const EXCLUDE_TYPES = array('RuntimeMarkup', 'RuntimeOnly', 'DbMigrateRuntime', 'MotifRuntime');
/*
* Field and template attributes to always ignore in migrations
*
*/
const EXCLUDE_ATTRIBUTES = array('_importMode', 'repeaterFields', '_lazy', '_exportMode');
// 'parent_id', template_id' and 'template_ids' are handled specifically (replaced with 'parent_path', 'template_name' and 'template_names') so no need to exclude here
/**
* Construct
* Set default values for configuration settings
*/
public function __construct() {
parent::__construct();
$this->set('auto_install', 1);
$this->set('prevent_overlap', 1);
$this->set('enable_dbMigrate', 1);
$this->set('install_repeats', 3);
}
public static function dbMigrateFields() {
return wire('fields')->find("tags=dbMigrate");
}
public static function dbMigrateTemplates() {
return wire('templates')->find("tags=dbMigrate");
}
/**
* Get a selector for all templates with the dbMigrate tag
* @return string
*/
public static function dbMigrateTemplateSelector() {
$dbmTemplates = implode('|', self::dbMigrateTemplates()->explode('name'));
// return wire('pages')->find("template=$dbmTemplates");
return "template={$dbmTemplates}";
}
/**
* Initialize the module
*
* ProcessWire calls this method when the module is loaded. At this stage, all
* module configuration values have been populated.
*
* For “autoload” modules (such as this one), this will be called before ProcessWire’s API is ready.
* This is a good place to attach hooks (as is the “ready” method).
*
*/
public function init() {
parent::init();
$this->bd('init from ProcessDbMigrate.php');
require_once('DbMigrationPage.class.php');
require_once('DbComparisonPage.class.php');
// Set properties
$this->set('adminPath', wire('pages')->get(2)->path());
$this->set('adminUrl', wire('pages')->get(2)->url());
$this->set('adminHttpUrl', wire('pages')->get(2)->httpUrl());
$this->set('name', self::moduleInfo()['page']['name']);
$this->set('parent', $this->adminPath . self::moduleInfo()['page']['parent'] . '/');
$this->set('parentUrl', $this->adminUrl . self::moduleInfo()['page']['parent'] . '/');
$this->set('parentHttpUrl', $this->adminHttpUrl . self::moduleInfo()['page']['parent'] . '/');
$this->set('title', self::moduleInfo()['page']['title']);
$this->set('adminProcess', str_replace(__NAMESPACE__ . '\\', '', get_class($this)));
$this->set('migrations', wire('pages')->get($this->adminPath . self::MIGRATION_PARENT));
$this->set('comparisons', wire('pages')->get($this->adminPath . self::COMPARISON_PARENT));
$this->set('migrationTemplate', wire('templates')->get(self::MIGRATION_TEMPLATE));
$this->set('comparisonTemplate', wire('templates')->get(self::COMPARISON_TEMPLATE));
$this->set('migrationsTemplate', wire('templates')->get(self::MIGRATION_PARENT_TEMPLATE));
$this->set('migrationsPath', wire('config')->paths->templates . self::MIGRATION_PATH);
$this->set('comparisonsPath', wire('config')->paths->templates . self::COMPARISON_PATH);
$this->set('modulePath', wire('config')->paths->siteModules . basename(__DIR__) . '/');
$this->set('bootstrapPath', $this->modulePath . 'bootstrap');
$this->set('bootstrap', wire()->pages->get("parent=$this->migrations, template=$this->migrationTemplate, name=bootstrap"));
// Need custom uninstall to uninstall bootstrap before uninstalling the module
$this->addHookBefore("Modules::uninstall", $this, "customUninstall");
// trigger init in Page class as it is not auto
if(class_exists('DbMigrationPage')) {
$p = $this->wire(new DbMigrationPage());
$p->init();
}
if(class_exists('DbComparisonPage')) {
$p = $this->wire(new DbComparisonPage());
$p->init();
}
/*
* The following hooks need to be here in init() as the hooked methods are called before ready()
*/
// Make sure any migration page is refreshed before loading it and set the 'updated' meta (used to indicate if refresh completed fully)
$this->addHookBefore('ProcessPageEdit::loadPage', function(HookEvent $event) {
$id = $event->arguments(0);
$p = $this->pages->get($id);
if($p and $p->id and ($p->template == self::MIGRATION_TEMPLATE or $p->template == self::COMPARISON_TEMPLATE)) {
/* @var $p DbMigrationPage */
$p->meta('updated', false);
$this->wire('process', $this); // Sets the process to ProcessDbMigrate, rather than ProcessPageEdit, which can cause problems in the refresh
$this->bd($this->wire()->process, 'process module refresh');
$updated = $p->refresh();
$this->bd($updated, 'meta updated');
$p->meta('updated', $updated);
}
});
/*
* To track which host page containing a Page Table field is currently being edited
* NB the Ajax hook needs to be in init() as it is called before ready()
*/
$this->addHookAfter('InputfieldPageTable::render', $this, 'handlePageTable');
// The ajax hook needs to be 'before' as ajax exits without triggering the after hook?
$this->addHookBefore('InputfieldPageTableAjax::checkAjax', $this, 'handlePageTable');
$this->set('trackingMigration', $this->getTrackingMigration());
$this->set('trackingField', null);
// CONFIGURE HELP TEMPLATE
$t = $this->templates->get('DbMigrateHelp');
if(!$t) return;
$moduleName = basename(__DIR__);
$t->filename = $this->config->paths->$moduleName . 'DbMigrateHelp.php';
$this->bd('ProcessDbMigrate INIT DONE');
}
/**
* Called when ProcessWire’s API is ready (optional)
*
* This optional method is similar to that of init() except that it is called
* after the current $page has been determined and the API is fully ready to use.
* Use this method instead of (or in addition to) the init() method if your
* initialization requires that the `$page` API variable is available.
*
* @throws WireException
*
*/
public function ready() {
$this->bd('ready from ProcessDbMigrate.php');
$currentUser = wire()->users->getCurrentUser();
if(!$currentUser->hasRole(self::moduleInfo()['permission']) && !$currentUser->isSuperuser()) return;
$this->bd("READY");
Debug::startTimer('from ready');
$val = Debug::timer('ready');
$this->addHookAfter('ProcessPageEdit::buildFormContent', $this, 'afterBuildFormContent');
if(!$this->enable_dbMigrate) return;
if(!$this->suppress_hooks) { // Skip all the hooks below to speed up the site (set in config). Functionality is lost but is not required unless migrations are required.
// // Code below is used in various hooks to detect changes to objects
$this->wire()->session->set('processed', []); // clear the session var that stores processed item ids
$this->wire()->session->set('processed_repeater', []); // clear the session var that stores processed repeater item ids
$this->wire()->session->set('processed_fieldgroup', []);
$this->bd($this->wire()->session->get('processed'), 'processed in ready' );
$this->wire()->setTrackChanges(Wire::trackChangesValues);
$val = Debug::timer('ready');
$this->bd($val, 'ready timer 4');
//
$this->addHookBefore("Pages::save", $this, 'beforeSave');
$this->addHookAfter("Pages::save", $this, 'afterSave');
$this->addHookAfter("Fieldtype::exportConfigData", $this, 'afterExportConfigData');
$this->addHookAfter('ProcessTemplate::buildEditForm', $this, 'afterTemplateBuildEditForm');
$this->addHookAfter('ProcessField::buildEditForm', $this, 'afterFieldBuildEditForm');
$this->addHookBefore('Templates::save', $this, 'beforeSaveTemplate');
$this->addHookBefore('Fields::save', $this, 'beforeSaveField');
// Hooks below are to handle field and template changes where 'log changes' has been enabled
// The functions they call are grouped together and documented there
// clear any old 'current' meta values
if($this->trackingMigration && $this->trackingMigration->id) {
foreach($this->trackingMigration->meta()->getArray() as $metaKey => $metaValue) {
$this->bd($metaKey, 'remove meta key?');
if(strpos($metaKey, 'current') === 0) {
$this->trackingMigration->meta()->remove($metaKey);
}
}
// $this->addHookAfter('Fieldgroups::saveReady()', $this, 'afterSaveReadyFieldgroup'); // not necessary as Fieldgroups extends WireSaveableItems
$this->addHookAfter('WireSaveableItems::saveReady', $this, 'hookMetaSaveable');
$this->addHookAfter('WireSaveableItems::renameReady', $this, 'hookMetaSaveable');
$this->addHookBefore('FieldtypeRepeater::deleteField', $this, 'setMetaDeleteRepeater');
$this->addHookBefore('Fields::saveFieldgroupContext', $this, 'beforeFieldsSaveFieldgroupContext');
$this->addHookAfter('WireSaveableItems::added', $this, 'handleSaveableHook');
$this->addHookAfter('WireSaveableItems::saved', $this, 'handleSaveableHook');
// ToDo move into a method
$this->addHookBefore('Fieldgroups::save', function($event) {
$object = $event->object;
$migration = $this->trackingMigration;
$fg = $event->arguments(0);
$this->bd($fg, 'new fg');
$fg1 = $this->wire('fieldgroups')->getFreshSaveableItem($fg);
$this->bd($fg1, 'fg1 orig saveable');
$tps = $fg->getTemplates();
foreach($tps as $tp) {
$this->bd($tp, 'tp new saveable');
$tp1 = $this->wire('templates')->getFreshSaveableItem($tp);
$tp1->setFieldgroup($fg1);
$this->bd($tp1, 'tp1 orig saveable');
if($tp1) {
$this->getObjectData($event, $tp1);
} else {
$migration->meta()->set("current_{$object}_{$tp->id}", []);
}
}
});
$this->addHookAfter('WireSaveableItems::renamed', $this, 'handleSaveableHook');
$this->addHookAfter('WireSaveableItems::deleteReady', $this, 'handleSaveableHook');
$this->addHookAfter('Fieldgroups::fieldRemoved', $this, 'afterFieldRemoved');
$this->addHookAfter('Fields::saveFieldgroupContext', $this, 'afterFieldsSaveFieldgroupContext');
$this->addHookAfter('Fieldgroups::save', $this, 'afterFieldgroupsSave');
// and these to handle page changes
//(meta for current is set in beforeSave hook)
$this->addHookAfter('Pages::added', $this, 'handleSaveableHook');
$this->addHookAfter('Pages::saved', $this, 'handleSaveableHook');
$this->addHookAfter('Pages::renamed', $this, 'handleSaveableHook');
$this->addHookAfter('Pages::deleteReady', $this, 'handleSaveableHook');
$this->addHookBefore('Pages::delete', $this, 'beforeDeletePage');
// need a 'getFresh' for fields and templates to track changes fully
$this->addHook('WireSaveableItems::getFreshSaveableItem', $this, 'getFreshSaveableItem');
}
}
$val = Debug::timer('ready');
$this->bd($val, 'ready timer 5');
// This hook needs to run regardless
$this->addHookBefore('ProcessPageEdit::execute', $this, 'beforePageEditExecute');
$this->bd('ProcessDbMigrate: ALL HOOKS RUN');
// Trigger ready() in the Page Class (actually trigger all page classes)
$page = $this->wire()->page;
if($page and $page->template == 'admin') {
$pId = $this->wire()->input->get->int('id');
$pId = $this->wire('sanitizer')->int($pId);
$p = ($pId > 0) ? $this->wire('pages')->get($pId) : null;
}
if(isset($p) and $p and $p->id and method_exists($p, 'ready')) $p->ready();
$val = Debug::timer('ready');
$this->bd($val, 'ready timer 6');
// Show the database name if selected in config, but only for admins with permission (and superuser)
if($this->dbName() and $this->show_name and wire('user')->hasPermission('admin-dbMigrate')) {
$this->wire()->message('DATABASE NAME = ' . $this->dbName());
}
// Load .js file and pass variables
$this->wire()->config->scripts->add($this->wire()->urls->siteModules . 'ProcessDbMigrate/ProcessDbMigrate.js');
$this->wire()->config->styles->add($this->wire()->urls->siteModules . 'ProcessDbMigrate/ProcessDbMigrate.css');
$this->wire->config->js('ProcessDbMigrate', [
'confirmDelete' => $this->_('Please confirm that this migration is not used in any other database before deleting it.
If it has been used in another environment and is no longer wanted then you will need to remove any orphan json files there manually.')
]);
$val = Debug::timer('ready');
$this->bd($val, 'ready timer 7');
/*
* Install the bootstrap if it exists, is installable and is not installed
* NB this cannot be done as part of install() as not all the required API is present until ready() is called
*/
if($this->auto_install) {
$this->bd("auto_install running! it runs anytime we view the database migrations page!!!");
$temp = $this->migrationTemplate;
$bootstrap = $this->wire()->pages->get("template=$temp, name=bootstrap");
/* @var $bootstrap DbMigrationPage */
$this->bd($bootstrap, 'bootstrap');
if($bootstrap && $bootstrap->id && $bootstrap->meta('installable') &&
(!isset($bootstrap->meta('installedStatus')['installed']) || !$bootstrap->meta('installedStatus')['installed'])) {
// if($this->session->get('upgraded')) {
// try {
// $bootstrap->installMigration('new');
// } catch(WirePermissionException $e) {
// $this->wire()->session->warning($this->_('You do not have permission'));
// } catch(WireException $e) {
// $this->wire()->session->warning($this->_('Unable to install bootstrap'));
// }
// } else {
try {
$this->wire()->session->warning($this->_('Bootstrap not fully installed - Attempting re-install.'));
$bootstrap->of(false);
$bootstrap->meta()->remove('filesHash');
$this->bd($bootstrap,'removed hash');
$bootstrap->refresh();
$this->bd($bootstrap, 'refreshed');
$bootstrap->installMigration('new');
$this->bd($bootstrap, 'installed');
} catch(WirePermissionException $e) {
$this->wire()->session->warning($this->_('You do not have permission'));
} catch(WireException $e) {
$this->wire()->session->warning($this->_('Unable to refresh bootstrap'));
}
// }
// $this->session->remove('upgraded');
}
if($this->session->get('upgrade0.1.0')) {
// remove the old RuntimeOnly fields if upgrading to 0.1.0
foreach(['dbMigrateActions', 'dbMigrateControl', 'dbMigrateReady'] as $name) {
$object = $this->wire('fields')->get($name);
if($object) {
$n = $object->name;
$object->flags = Field::flagSystemOverride;
$object->flags = 0;
$object->save();
try {
$this->wire('fields')->delete($object);
} catch(WireException $e) {
$this->wire()->session->error('Object: ' . $object . ': ' . $e->getMessage());
$this->bd($n, 'ERROR IN DELETION - ' . $e->getMessage());
}
}
}
$this->session->remove('upgrade0.1.0');
}
}
$val = Debug::timer('ready');
$this->bd($val, 'ready timer 8');
// /*
// * Make sure all the assets are there for a page with repeaters
// * MOVED to beforePageEditExecute to stop it executing on saves and causing corruption issues
// */
// $page = $this->page();
// if($page and $page->template == 'admin') {
// $pId = $this->wire()->input->get('id');
// $pId = $this->wire('sanitizer')->int($pId);
// if(is_int($pId) && $pId > 0) {
// $p = $this->wire('pages')->get($pId);
//
// $this->getInputfieldAssets($p);
// }
// }
$this->bd('ProcessDbMigrate: READY DONE');
}
/**
* Set the meta('current') for the specified object, where it is a field or template
*
* @param $event
* @param $obj
* @return void
* @throws WireException
*/
public function getObjectData($event, $obj) {
// NB The code below only deals with template and field objects. Pages are handled separately in beforeSave.
$migration = $this->trackingMigration;
$val = Debug::timer('ready');
$this->bd($val, 'ready timer 1');
$type = $typeName = $tracking = null;
$this->objectType($event, $obj, $type, $typeName, $tracking);
$this->bd(['obj' => $obj, 'type' => $type, 'typeName' => $typeName, 'tracking' => $tracking], 'objectType returned');
if(!$type) return;
$object = ($typeName == 'fields') ? 'field' : (($typeName == 'templates') ? 'template' : null);
if(!$object) return;
$scopedObjects = $this->wire()->$typeName->find($migration->$tracking);
$val = Debug::timer('ready');
$this->bd($val, 'ready timer 2');
if($migration && $migration->id && $scopedObjects->has($obj)) {
// Ignore templates and fields which belong to DbMigrate itself
if(wireInstanceOf($obj, 'Template') && self::dbMigrateTemplates()->has($obj)) return;
if(wireInstanceOf($obj, 'Field') && self::dbMigrateFields()->has($obj)) return;
$objectData = $this->getExportDataMod($obj);
$this->bd($objectData, 'objectData set to meta current');
if(!$migration->meta()->get("current_{$object}_{$obj->id}")) $migration->meta()->set("current_{$object}_{$obj->id}", $objectData);
$migMeta = $migration->meta()->getArray();
$val = Debug::timer('ready');
$this->bd(['object' => $obj->name, 'migration meta' => $migMeta, 'timer' => $val], 'ready timer 3');
//$this->wire()->log->save('debug', "timer 3 for {$obj->name} is $val");
}
$this->bd($migration->meta()->getArray(), 'meta for ' . $migration->name);
}
/**
* Add environment to the database name, as required
*/
public function dbName() {
if($this->append_env) {
return (isset($this->wire()->config->dbMigrateEnv)) ? $this->database_name . $this->wire()->config->dbMigrateEnv : $this->database_name;
} else {
return $this->database_name;
}
}
/**
* Make sure all the assets are there for a page with repeaters
*
* @param $p
* @return void
*/
protected function getInputfieldAssets($p) {
foreach($p->getFields() as $field) {
$inputfield = $field->getInputfield($p);
$type = $inputfield->className;
$name = $inputfield->attr('name');
if($type == 'InputfieldRepeaterMatrix' || $type == 'InputfieldRepeater') {
$repeater = $p->$name;
if(!wireInstanceOf($repeater, 'PageArray')) { // In case repeater is a FieldsetPage, so not an array
$repeater = [$repeater];
}
foreach($repeater as $repeaterItem) {
$repeaterItem->of(false);
$this->getInputfieldAssets($repeaterItem);
}
} else {
$p->of(false);
$inputfield->renderReady();
$this->bd(['page' => $p, 'inputfield' => $inputfield], 'inputfield');
}
}
}
/**
* NOT USED
*
* @param $event
* @return void
* @throws WireException
*/
protected function afterMoveable($event) {
$this->bd($event, 'IN MOVEABLE HOOK');
/** @var Page $page */
$page = $event->object;
$moveable = $event->return;
foreach($this->migrations->find("template={$this->migrationTemplate}, include=all") as $migration) {
/* @var $migration DbMigrationPage */
if($migration->conflictFree()) continue;
$migrationNames = $migration->itemNames('page');
$this->bd($migrationNames, 'migration names');
if(in_array($page->path, $migrationNames)) {
$this->bd($page, 'NOT MOVEABLE');
$moveable = false;
}
}
$event->return = $moveable;
}
// protected function listAllFiles($dir) {
// $assocArray = [];
// if(scandir($dir)) {
// $array = array_diff(scandir($dir), array('.', '..'));
// foreach($array as $item) {
// $assocArray[$item] = $dir . $item;
// }
// unset($item);
// foreach($assocArray as $item => $path) {
// if(is_dir($path)) {
// $assocArray = array_merge($assocArray, $this->listAllFiles($path . DIRECTORY_SEPARATOR));
// }
// }
// }
// return $assocArray;
// }
/**
*
* Install/upgrade the module
*
* @param false $upgrade
* @throws WireException
* @throws WirePermissionException
*
*/
public function ___install($upgrade = false) {
$this->bd('ProcessDbMigrate install');
$enable = $this->enable_dbMigrate;
$suppress = $this->suppress_hooks;
if(!$enable) $this->set('enable_dbMigrate', 1);
if($suppress) $this->set('suppress_hooks', 0);
$this->init(); // Need the properties to be loaded for the bootstrap, but init() is not called before install()
$this->bootstrap($upgrade);
$this->init(); // re-initialise now everything should be there
$this->set('enable_dbMigrate', $enable);
$this->set('suppress_hooks', $suppress);
if(!$this->migrations or !$this->migrations->id) {
$this->wire->session->error($this->_('Bootstrap failed'));
return;
}
$setupPage = $this->wire('pages')->get("template=admin, name={$this->name}");
if(!$setupPage or !$setupPage->id) {
// Create the admin page
$tpl = $this->wire()->templates->get('admin');
$p = $this->wire(new Page($tpl));
$p->name = $this->name;
$p->parent = $this->wire('pages')->get("name=setup, parent.id=2");;
$p->title = $this->title;
$p->process = $this->adminProcess;
$p->of(false);
$p->save();
}
/* CONSTRUCT HELP TEMPLATE AND PAGE
* Kept within the module directory (see forum post https://processwire.com/talk/topic/2676-configuring-template-path/page/3/)
*/
$this->createTemplate('DbMigrateHelp', 'DbMigrateHelp');
$this->createPage('DbMigrate help', 'DbMigrateHelp');
}
/**
*
* Upgrade the module
*
* @param int|string $fromVersion
* @param int|string $toVersion
* @throws WireException
* @throws WirePermissionException
*
*/
public function ___upgrade($fromVersion, $toVersion) {
$this->bd([$fromVersion, $toVersion], 'upgrade - from, to');
$this->session->set('upgraded', true);
// Versions >= 0.1.0 use FieldtypeDbMigrateRuntime not RuntimeOnly
if(version_compare($toVersion, '0.1.0', '>=')
and version_compare($fromVersion, '0.1.0', '<')) {
// add the new fieldtype
$this->wire()->modules->install('FieldtypeDbMigrateRuntime');
// set session var to allow removal of old fields
$this->session->set('upgrade0.1.0', true); // session var set here is used in ready() as old fields cannot be removed until after bootstrap install
}
// Versions >= 2.0.0 have tracking scope in the migration page, not the module
if(version_compare($toVersion, '2.0.0', '>=')
and version_compare($fromVersion, '2.0.0', '<')) {
$this->warning($this->_("Scope of tracking for migrations using 'log changes' will be lost as this is now (post v2.0.0) maintained at the migration page level.\n
For any open migrations, you may need to re-set the tracking scope on the migration page."));
}
$this->___install(true);
}
/**
* Install the bootstrap page
*
* @param boolean $upgrade True for upgrade only.
* @throws WireException
* @throws WirePermissionException
*
*/
protected function bootstrap($upgrade = false) {
/*
* For an upgrade, we need to see if the bootstrap json files have changed.
* If they have, we will re-install the bootstrap using the new files, just like a new install
* If they haven't we don't need to do anything - our bootstrap is still good
*/
$this->bd($upgrade, 'bootstrap (upgrade?)');
$sameBootstrap = false;
$temp = $this->migrationTemplate;
$bootstrap = $this->wire()->pages->get("template=$temp, name=bootstrap");
$this->bd($bootstrap, 'bootstrap');
/* @var $bootstrap DbMigrationPage */
if($upgrade) {
if($bootstrap && $bootstrap->id) {
$currentFilesHash = $bootstrap->filesHash();
$newFilesHash = $bootstrap->filesHash($this->modulePath);
$sameBootstrap = ($currentFilesHash == $newFilesHash);
$this->bd([$currentFilesHash, $newFilesHash], 'current & new hashes');
}
}
$this->bd($sameBootstrap, 'samebootstrap');
if($sameBootstrap) return;
/*
* We have a new bootstrap to install
*/
// copy the bootstrap files to templates directory
if(!is_dir($this->migrationsPath . 'bootstrap/')) if(!wireMkdir($this->migrationsPath . 'bootstrap/', true)) {
throw new WireException($this->_('Unable to create migration directory') . ": {$this->migrationsPath}bootstrap/");
}
$this->bd(['bootstrapPath' => $this->bootstrapPath, 'migrationsPath' => $this->migrationsPath . 'bootstrap/'], 'copy files');
$this->wire()->files->copy($this->bootstrapPath, $this->migrationsPath . 'bootstrap/');
// Before we are able to use the copied .json files, we need to check and amend the admin root in use as it may differ in the target system
if($this->adminPath != self::SOURCE_ADMIN) {
$jsonFiles = ['/new/data.json', '/new/migration.json', '/old/data.json', '/old/migration.json'];
foreach($jsonFiles as $jsonFile) {
$json = (file_exists($this->migrationsPath . 'bootstrap' . $jsonFile))
? file_get_contents($this->migrationsPath . 'bootstrap' . $jsonFile) : null;
if($json) {
$json = str_replace(self::SOURCE_ADMIN, $this->adminPath, $json);
file_put_contents($this->migrationsPath . 'bootstrap' . $jsonFile, $json);
}
}
}
// Delete any old bootstrap before continuing
if($bootstrap && $bootstrap->id) {
if($bootstrap->isLocked()) $bootstrap->removeStatus(Page::statusLocked);
$bootstrap->delete(true);
$this->bd('deleted bootstrap');
}
// Install a dummy bootstrap using the copied/amended files
$className = 'ProcessWire\\' . self::MIGRATION_TEMPLATE . 'Page';
$dummyBootstrap = $this->wire(new $className()); // dummy migration
/*
* NB we cannot assign a template to dummy-bootstrap as we need to run it to create the template!!
*/
$dummyBootstrap->name = 'dummy-bootstrap';
/* @var $dummyBootstrap DbMigrationPage */
$dummyBootstrap->installMigration('new');
$this->bd('installed new bootstrap');
/* NB For normal upgrades, bootstrap installation is run by ready() if bootstrap is not installed
It cannot be run here as not all the API is present
*/
}
/**
* Custom uninstall routine
*
* @param $event
* @throws WireException
*
*/
public function customUninstall($event) {
$class = $event->arguments(0);
if(__NAMESPACE__ . '\\' . $class != __CLASS__) return;
$this->bd('IN CUSTOM UNINSTALL - uninstalling....');
$abort = false;
// Make sure there is a bootstrap page
$setupPage = $this->wire()->pages->get($this->adminPath . 'setup/dbmigrations/');
if($setupPage and $setupPage->id) {
if(!$this->bootstrap or !$this->bootstrap->id) {
$this->error($this->_('Uninstall of bootstrap failed.') . "\n" .
$this->_('No bootstrap page - try going to setup page and refreshing before uninstalling'));
$abort = true;
}
} else {
$this->bd('NO SETUP PAGE');
return;
}
//Check all templates with dbMigrate tags to ensure there are no migration pages
$taggedTemplates = wire()->templates->find("tags=dbMigrate");
$templateList = $taggedTemplates->implode('|', 'name');
$pagesUsingTemplates = wire()->pages->find("template=$templateList, include=all"); // gets trashed pages as well
if($pagesUsingTemplates and $pagesUsingTemplates->count() > 0) {
$links = "";
foreach($pagesUsingTemplates as $pageUsingTemplate) {
if(
($pageUsingTemplate->template == self::MIGRATION_TEMPLATE or $pageUsingTemplate->template == self::COMPARISON_TEMPLATE)
and
$pageUsingTemplate->name != 'bootstrap'
) {
$links .= "<li><a target='_blank' href='{$pageUsingTemplate->editUrl}'>{$pageUsingTemplate->get('title|name')}</a></li>";
}
}
}
if($links) {
$this->error(
// Delete all comparison pages and all migration pages except for bootstrap before uninstalling.
$this->_('Uninstall aborted as there are existing migration and/or comparison pages.') .
"<p class='uk-text-small uk-margin-small-top uk-margin-remove-bottom'>The following pages need to be trashed and completely deleted first:</p>" .
"<ul class='uk-list-disc uk-text-small uk-margin-remove-top'>".$links."</ul>",
Notice::allowMarkup
);
$event->replace = true; // prevents original uninstall
$this->session->redirect("./edit?name=$class"); // prevent "module uninstalled" message
}
// unset system flags on templates and fields
$dbMigrateTemplates = wire()->templates->find("tags=dbMigrate");
foreach($dbMigrateTemplates as $t) {
$t->flags = Template::flagSystemOverride;
$t->flags = 0;
foreach($t->fieldgroup as $f) {
$f->flags = Field::flagSystemOverride;
$f->flags = 0;
}
}
$dbMigrateFields = wire()->fields->find("tags=dbMigrate");
foreach($dbMigrateFields as $f) {
$f->flags = Field::flagSystemOverride;
$f->flags = 0;
}
// uninstall if there is a valid 'old' directory
if(!$abort) {
$this->bootstrap->ready(); // Need the properties to be loaded for the uninstall, but ready() is not called before uninstall()
// remove the help page etc
$helpPages = $this->pages()->find("template=DbMigrateHelp");
foreach($helpPages as $helpPage) {
$helpPage->delete(true);
}
$helpTemplate = $this->templates()->get('DbMigrateHelp');
$fieldgroup = $helpTemplate->fieldgroup;
$this->wire('templates')->delete($helpTemplate);
$this->wire('fieldgroups')->delete($fieldgroup);
// Uninstall all the bootstrap items
try {
$this->bd('uninstalling bootstrap');
$this->bootstrap->installMigration('old');
} catch(WireException $e) {
$this->bd($e, 'WireException');
$msg = $e->getMessage();
$this->error($this->_('Uninstall of bootstrap failed or incomplete.') . "\n $msg \n" .
$this->_('Re-install the module and fix the cause of the problem before uninstalling again.'));
$abort = false; //allow uninstall to complete as re-installation of bootstrap may be required to enable proper uninstallation
}
$this->message(sprintf($this->_('Removed %s'), $setupPage->path));
$this->wire()->pages->delete($setupPage, true);
}
// uninstall?
if($abort) {
$this->bd('ABORTING UNINSTALL');
// there were some non-critical errors
// close without uninstalling module -
$event->replace = true; // prevents original uninstall
$this->session->redirect("./edit?name=$class"); // prevent "module uninstalled" message
}
}
/**
* Main admin page - list migrations & allows creation of new migration
*
* @return array|string
* @throws WireException
* @throws WirePermissionException
*
*/
public function ___execute() {
if(!$this->enable_dbMigrate) {
$this->wire()->error("ProcessDbMigrate is disabled - it can be enabled in the module settings.");
return;
}
if($this->suppress_hooks) $this->wire()->error("Hook suppression is on - migrations will not work correctly - unset in the module settings.");
$pageEdit = $this->wire('urls')->admin . 'page/edit/?id=';
$this->modules->get('JqueryWireTabs');
/* @var $form InputfieldForm */
$form = $this->modules->get("InputfieldForm");
$form->attr('id', 'ProcessDbMigrate');
// tab 1 - Migrations
$tab = new InputfieldWrapper();
$tab->attr('id', 'migrations');
$tab->attr('title', 'Migrations');
$tab->attr('class', 'WireTab');
$field = $this->modules->get("InputfieldMarkup");
$table = $this->wire('modules')->get("MarkupAdminDataTable");
$table->headerRow(['Name', 'Type', 'Status', 'Title', 'Summary', 'Items', 'Created']);
$table->setSortable(true);
$table->setEncodeEntities(false);
$this->moduleRefresh();
$migrationPages = $this->migrations->find("template=$this->migrationTemplate, sort=-created, include=all");
foreach($migrationPages as $migrationPage) {
/* @var $migrationPage DbMigrationPage */
if($migrationPage && $migrationPage->id) {
$installedStatus = $migrationPage->meta('installedStatus');
$status = ($installedStatus) ? $installedStatus['status'] : 'indeterminate';
if(!$migrationPage->meta('locked')) {
if($migrationPage->meta('installable')) {
$statusColour = ($status == 'installed') ? 'lightgreen' : (($status == 'uninstalled') ? 'salmon' : 'orange');
} else {
$statusColour = ($status == 'exported') ? 'lightgreen' : 'salmon';
}
} else {
// $status = 'Locked';
$statusColour = 'LightGrey';
}
$this->bd($migrationPage, $status);
$this->bd($installedStatus);
$lockIcon = ($migrationPage->meta('locked')) ? '<i class="fa fa-lock"></i>' : '<i class="fa fa-unlock"></i>';
$itemList = [];
//foreach($migrationPage->getFormatted('dbMigrateItem') as $migrateItem) {
foreach($migrationPage->dbMigrateItem->find("status=1") as $migrateItem) {
/* @var $migrateItem RepeaterPage */
$oldName = ($migrateItem->dbMigrateOldName) ? '|' . $migrateItem->dbMigrateOldName : '';
$itemList[] = '<em>' . $migrateItem->dbMigrateAction->title . ' ' . $migrateItem->dbMigrateType->title . '</em>: ' . $migrateItem->dbMigrateName . $oldName;
}
$itemsString = implode(" ", $itemList);
$magic = (!$migrationPage->meta('installable') && !$migrationPage->meta('locked') && $migrationPage->dbMigrateLogChanges == 1) ? '<span class="fa fa-magic"></span>' : '';
$data = array(
// Values with a string key are converter to a link: title => link
$migrationPage->name => $pageEdit . $migrationPage->id,
($migrationPage->meta('installable')) ? '<span class="fa fa-arrow-down"></span>' : '<span class="fa fa-arrow-up"></span>' . $magic,
$lockIcon . ' <span style="background:' . $statusColour . '">' . $status . '</span>',
$migrationPage->title,
[$migrationPage->dbMigrateSummary, 'migration-table-text'],
[$itemsString, 'migration-table-text'],
date('Y-m-d', $migrationPage->created),
);
$table->row($data);
}
}
$this->wire('modules')->get('JqueryUI')->use('modal');
$out = '<div><h3>' . $this->_('Existing migrations are listed below. Go to the specific migration page for any actions.') . '</h3><p>' .
$this->_('Exportable migrations') . ' - <span class="fa fa-arrow-up"></span> - ' .
$this->_('originated in this database, can be edited here and are a source of a migration to be installed elsewhere.') . '</p><p>' .
$this->_('------------------------') . ' - <span class="fa fa-magic"></span> - ' .
$this->_('Indicates that "log changes" is active for this (exportable) migration.') . '</p><p>' .
$this->_('Installable migrations') . ' - <span class="fa fa-arrow-down"></span> - ' .
$this->_('originated from another database and can be installed/uninstalled here (except that "bootstrap" cannot be uninstalled). They cannot be changed except in the original database.') . '</p><p>' .
$this->_('Locked migrations') . ' - <span class="fa fa-lock"></span> - ' .
$this->_('can no longer be changed or actioned.') . '</p></div><div>';
$out .= $table->render();
$btnAddNew = $this->createNewButton($this->migrationTemplate, $this->migrations); //createNewButton also allows title and values to be set, but not used here
$out .= $btnAddNew->render();
$btn = $this->wire('modules')->get("InputfieldButton");
$btn->attr('href', './get-migrations/');
$btn->attr('id', "get_migrations");
$btn->attr('value', "Refresh migrations");
$btn->showInHeader();
$out .= $btn->render();
$field->value = $out;
$tab->add($field);
$form->append($tab);
// tab 2 - Comparisons
$tab = new InputfieldWrapper();
$tab->attr('id', 'database-comparisons');
$tab->attr('title', 'Database Comparisons');
$tab->attr('class', 'WireTab');
$field = $this->modules->get("InputfieldMarkup");
/* @var $field InputfieldMarkup */
$field->attr('id+name', 'database-comparisons');
$table = $this->wire('modules')->get("MarkupAdminDataTable");
$table->headerRow(['Name', 'Source DB', 'Title', 'Summary', 'Items', 'Created']);
$table->setSortable(true);
$table->setEncodeEntities(false);
$comparisonPages = $this->comparisons->find("template=$this->comparisonTemplate, sort=-created, include=all");
foreach($comparisonPages as $comparisonPage) {
/* @var $comparisonPage DbComparisonPage */
if($comparisonPage && $comparisonPage->id) {
$itemList = [];
foreach($comparisonPage->dbMigrateComparisonItem as $comparisonItem) {
/* @var $comparisonItem RepeaterPage */
$itemList[] = '<em>' . $comparisonItem->dbMigrateType->title . '</em>: ' . $comparisonItem->dbMigrateName;
}
$itemsString = implode(" ", $itemList);
$data = array(
// Values with a string key are converter to a link: title => link
$comparisonPage->name => $pageEdit . $comparisonPage->id,
$comparisonPage->meta('sourceDb'),
$comparisonPage->title,
$comparisonPage->dbMigrateSummary,
$itemsString,
date('Y-m-d', $comparisonPage->created),
);
$table->row($data);
}
}
$this->wire('modules')->get('JqueryUI')->use('modal');
$out = '<div><h3>' . $this->_('Existing database comparison pages are listed below. Go to the specific page to compare.') . '</h3></div>';
$out .= $table->render();
$btnAddNew = $this->createNewButton($this->comparisonTemplate, $this->comparisons); //createNewButton also allows title and values to be set, but not used here
$out .= $btnAddNew->render();
$btn = $this->wire('modules')->get("InputfieldButton");
$this->bd($this->name, 'this name');
$btn->attr('href', $this->parentHttpUrl . $this->name . "/get-comparisons/");
$btn->attr('id', "get_comparisons");
$btn->attr('value', "Refresh comparisons");
$btn->showInHeader();
$out .= $btn->render();
$field->value = $out;
$tab->add($field);
$form->append($tab);