1: <?php
2:
3: namespace Aurora\System\Console\Commands\Migrations;
4:
5: use Aurora\System\Api;
6: use Aurora\System\Console\Commands\BaseCommand;
7: use Illuminate\Database\Capsule\Manager as DB;
8: use Symfony\Component\Console\Command\Command;
9: use Symfony\Component\Console\Helper\ProgressBar;
10: use Symfony\Component\Console\Input\InputDefinition;
11: use Symfony\Component\Console\Input\InputInterface;
12: use Symfony\Component\Console\Input\InputOption;
13: use Symfony\Component\Console\Logger\ConsoleLogger;
14: use Symfony\Component\Console\Output\OutputInterface;
15: use Symfony\Component\Console\Question\ConfirmationQuestion;
16: use Illuminate\Database\Capsule\Manager as Capsule;
17:
18: class EavToSqlCommand extends BaseCommand
19: {
20: private $sFilePrefix = 'eav-to-sql-';
21:
22: private $iOffset = 0;
23: private $iLimit = 1000;
24: private $oP8Settings = false;
25: private $logger = false;
26:
27: /**
28: * @return void
29: */
30: public function __construct()
31: {
32: parent::__construct();
33: \Aurora\Api::Init();
34: }
35:
36: protected function configure(): void
37: {
38: $this->setName('migrate:eav-to-sql')
39: ->setDescription('Migrate EAV data structure to SQL')
40: ->setDefinition(
41: new InputDefinition([
42: new InputOption('wipe', 'w'),
43: new InputOption('migrate-file', 'f'),
44: ])
45: );
46: }
47:
48: protected function getProperties($class, $object)
49: {
50: $extendedPropsUser = \Aurora\System\ObjectExtender::getInstance()->getExtendedProps($class);
51: $extendedProps = [];
52: foreach (array_keys($extendedPropsUser) as $extendedProp) {
53: if ($object->get($extendedProp)) {
54: $extendedProps[$extendedProp] = $object->get($extendedProp);
55: }
56: }
57: return $extendedProps;
58: }
59:
60: protected function rewriteFile($fd, $str)
61: {
62: ftruncate($fd, 0);
63: fseek($fd, 0, SEEK_END);
64: fwrite($fd, $str);
65: }
66:
67: protected function migrateMinHashes($oConnection)
68: {
69: if (!Capsule::schema()->hasTable('core_min_hashes')) {
70: return false;
71: }
72: if (!Capsule::schema()->hasTable('min_hashes')) {
73: return false;
74: }
75:
76: $sql = "TRUNCATE `" . $this->oP8Settings->DBPrefix . "core_min_hashes`;";
77: $oConnection->execute($sql);
78: $sql = "SELECT * FROM " . $this->oP8Settings->DBPrefix . "min_hashes;";
79: $oConnection->execute($sql);
80: if ($oConnection->ResultCount() > 0) {
81: $sql = "INSERT INTO " . $this->oP8Settings->DBPrefix . "core_min_hashes (`HashId`, `UserId`,`Hash`, `Data`, `ExpireDate`)
82: SELECT *
83: FROM " . $this->oP8Settings->DBPrefix . "min_hashes;";
84: $oConnection->execute($sql);
85: }
86: return true;
87: }
88:
89: protected function migrateActivityHistory($oConnection)
90: {
91: if (!Capsule::schema()->hasTable('core_activity_history')) {
92: return false;
93: }
94: if (!Capsule::schema()->hasTable('activity_history')) {
95: return false;
96: }
97: $sql = "TRUNCATE `" . $this->oP8Settings->DBPrefix . "core_activity_history`;";
98: $oConnection->execute($sql);
99: $sql = "INSERT INTO " . $this->oP8Settings->DBPrefix . "core_activity_history (`Id`, `UserId`, `ResourceType`,`ResourceId`, `IpAddress`, `Action`, `Timestamp`, `GuestPublicId`)
100: SELECT *
101: FROM " . $this->oP8Settings->DBPrefix . "activity_history;";
102: $oConnection->execute($sql);
103: return true;
104: }
105:
106: protected function wipeP9Tables()
107: {
108: $aModels = $this->getAllModels();
109: foreach ($aModels as $modelName => $modelPath) {
110: $model = str_replace('/', DIRECTORY_SEPARATOR, $modelPath);
111: $model = str_replace('\\', DIRECTORY_SEPARATOR, $model);
112: $model = explode(DIRECTORY_SEPARATOR, $model);
113: $modelClass = [];
114:
115: while ($model[0] !== 'modules') {
116: array_shift($model);
117: }
118: $model[0] = 'Modules';
119: array_unshift($model, "Aurora");
120: $model = implode('\\', $model);
121: $this->logger->info('wiping ' . $model::query()->getQuery()->from);
122: $model::truncate();
123: }
124: }
125:
126: protected function jsonPretify($sJsonStr)
127: {
128: $sOutput = '{';
129: $bFirstElement = true;
130: foreach ($sJsonStr as $key => $value) {
131: if (!$bFirstElement) {
132: $sOutput .= ",";
133: }
134: $bFirstElement = false;
135:
136: $sOutput .= PHP_EOL . "\t\"" . $key . "\": [";
137: $sOutput .= PHP_EOL . "\t\t" . implode(',', $value);
138: $sOutput .= PHP_EOL . "\t]";
139: }
140: $sOutput .= PHP_EOL . '}';
141: $sOutput = str_replace('\\', '\\\\', $sOutput);
142:
143: return $sOutput;
144: }
145:
146: protected function execute(InputInterface $input, OutputInterface $output): int
147: {
148: DB::statement('SET FOREIGN_KEY_CHECKS=0;');
149: $migrateEntitiesList = [];
150: $offset = 0;
151: $time = new \DateTime();
152: $time = $time->format('YmdHis');
153: $intProgress = 0;
154: $dirName = \Aurora\System\Api::DataPath() . "/migration-eav-to-sql";
155: $filename = $dirName . "/migration-" . $time . ".txt";
156: $progressFilename = $dirName . "/migration-progress.txt";
157: $entitiesListFilename = $dirName . "/migration-list.txt";
158: $missedEntitiesFilename = $dirName . "/migration-" . $time . "-missed-entities.txt";
159:
160: $dirname = dirname($filename);
161: if (!is_dir($dirname)) {
162: mkdir($dirname, 0755, true);
163: }
164:
165: $verbosityLevelMap = array(
166: 'notice' => OutputInterface::VERBOSITY_NORMAL,
167: 'info' => OutputInterface::VERBOSITY_NORMAL,
168: );
169:
170: $fdProgress = file_exists($progressFilename) ? fopen($progressFilename, 'r+') : false;
171: $this->logger = new ConsoleLogger($output, $verbosityLevelMap);
172: $this->oP8Settings = \Aurora\System\Api::GetSettings();
173: $helper = $this->getHelper('question');
174:
175: $wipe = $input->getOption('wipe');
176: $migrateFile = $input->getOption('migrate-file');
177: $defaultAnswer = $input->getOption('no-interaction');
178: if ($wipe) {
179: if (!$defaultAnswer) {
180: $question = new ConfirmationQuestion('Do you really wish to wipe all data in target tables? (Y/N)', $defaultAnswer);
181:
182: if (!$helper->ask($input, $output, $question)) {
183: return Command::SUCCESS;
184: }
185: }
186: $this->wipeP9Tables();
187: } elseif ($migrateFile) {
188: $sEntitiesList = @file_get_contents($entitiesListFilename, true);
189: if (!$sEntitiesList) {
190: $this->logger->error('Entities list is empty!');
191: return false;
192: }
193:
194: $migrateEntitiesList = explode(',', $sEntitiesList);
195: if (!$defaultAnswer) {
196: $question = new ConfirmationQuestion("Proceed with migrating " . count($migrateEntitiesList) . " entities? (Y/N)", $defaultAnswer);
197: if (!$helper->ask($input, $output, $question)) {
198: return Command::SUCCESS;
199: }
200: }
201: } else {
202: if ($fdProgress) {
203: $progress = htmlentities(file_get_contents($progressFilename));
204: $intProgress = intval($progress);
205: if ($intProgress) {
206: if (!$defaultAnswer) {
207: $question = new ConfirmationQuestion("Resume from entity with ID $intProgress (last successfully migrated)? (Y/N)", $defaultAnswer);
208: if (!$helper->ask($input, $output, $question)) {
209: return Command::SUCCESS;
210: }
211: }
212: $cItems = DB::Table('eav_entities')->select('id')->where('id', '<=', $intProgress)->get();
213: $offset = count($cItems);
214: } else {
215: if (!$defaultAnswer) {
216: $question = new ConfirmationQuestion("File $progressFilename is invalid. Do you wish migrate all entities? (Y/N)", $defaultAnswer);
217: if (!$helper->ask($input, $output, $question)) {
218: return Command::SUCCESS;
219: }
220: }
221: }
222: }
223: }
224:
225: $fdProgress = fopen($progressFilename, 'a+') or die("Can't create migration-progress.txt file");
226: $fdErrors = fopen($filename, 'w+') or die("Can't create migration-" . $time . ".txt file");
227: $fdMissedIds = fopen($missedEntitiesFilename, 'w+') or die("Can't create migration-" . $time . "-missed-entities.txt file");
228:
229: $sql = "SELECT id, entity_type FROM `" . $this->oP8Settings->DBPrefix . "eav_entities` GROUP BY id, entity_type";
230: $sqlIn = "";
231: foreach ($migrateEntitiesList as $entityId) {
232: $sqlIn .= $entityId . ',';
233: }
234: $sqlIn = substr($sqlIn, 0, -1);
235:
236: if ($migrateEntitiesList) {
237: $sql = "SELECT id, entity_type FROM `" . $this->oP8Settings->DBPrefix . "eav_entities` WHERE id IN ($sqlIn) GROUP BY id, entity_type;";
238: }
239:
240: $oConnection = \Aurora\System\Api::GetConnection();
241: $oConnection->execute($sql);
242:
243: $entities = [];
244: $passedEntities = 0;
245: while (false !== ($oRow = $oConnection->GetNextRecord())) {
246: if ($oRow->id <= $intProgress) {
247: $passedEntities++;
248: continue;
249: }
250: $entities[] = $oRow;
251: }
252:
253: $progressBar = new ProgressBar($output, $passedEntities + count($entities));
254: $progressBar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:6s% %memory:6s%');
255: $progressBar->start();
256: $progressBar->advance($passedEntities);
257:
258: $this->migrateActivityHistory($oConnection);
259: $this->migrateMinHashes($oConnection);
260: $result = $this->migrate($fdProgress, $fdErrors, $migrateEntitiesList, $entities, $progressBar, $fdMissedIds, $wipe);
261: $this->rewriteFile($fdErrors, $this->jsonPretify($result['MissedEntities']));
262: $this->rewriteFile($fdMissedIds, implode(',', $result['MissedIds']));
263:
264: fclose($fdMissedIds);
265: fclose($fdErrors);
266: fclose($fdProgress);
267: return Command::SUCCESS;
268: }
269:
270: private function migrate($fdProgress, $fdErrors, $migrateEntitiesList, $entities, $progressBar, $fdMissedIds, $wipe)
271: {
272: $missedEntities = [];
273: $missedIds = [];
274: $contactsCache = [];
275: $groupsCache = [];
276: $migrateArray = [];
277: $truncatedTables = [];
278:
279: $modelsMap = [
280: 'Aurora\Modules\Mail\Classes\Account' => 'Aurora\Modules\Mail\Models\MailAccount',
281: 'Aurora\Modules\OAuthIntegratorWebclient\Classes\Account' => 'Aurora\Modules\OAuthIntegratorWebclient\Models\OauthAccount',
282: 'Aurora\Modules\Mail\Classes\Sender' => 'Aurora\Modules\Mail\Models\TrustedSender'
283: ];
284:
285: foreach ($entities as $i => $entity) {
286: try {
287: $migrateArray = ['Id' => $entity->id];
288: if (!class_exists($entity->entity_type)) {
289: $missedEntities[$entity->entity_type][] = $entity->id;
290: $missedIds[] = $entity->id;
291: $this->logger->warning("Didn't find EAV class for entity with id $entity->id, skipping.");
292:
293: $progressBar->advance();
294: continue;
295: }
296:
297: $aItem = collect(
298: (new \Aurora\System\EAV\Query($entity->entity_type))
299: ->where(['EntityId' => $entity->id])
300: ->asArray()
301: ->exec()
302: )->first();
303: $laravelModel = $modelsMap[$entity->entity_type] ?? str_replace('Classes', 'Models', $entity->entity_type);
304:
305: switch ($entity->entity_type) {
306: case 'Aurora\Modules\StandardAuth\Classes\Account':
307: $oItem = collect((new \Aurora\System\EAV\Query($entity->entity_type))
308: ->where(['EntityId' => [$entity->id, '=']])
309: ->exec())->first();
310: $password = str_replace($oItem->Login, '', $oItem->Password);
311: $migrateArray['Password'] = $password;
312: break;
313:
314: case 'Aurora\Modules\Mail\Classes\Account':
315: $oItem = collect((new \Aurora\System\EAV\Query($entity->entity_type))
316: ->where(['EntityId' => [$entity->id, '=']])
317: ->exec())->first();
318: $migrateArray['IncomingPassword'] = $oItem->getPassword();
319: break;
320:
321: case 'Aurora\Modules\Contacts\Classes\GroupContact':
322: if (isset($contactsCache[$aItem['ContactUUID']])) {
323: $migrateArray['ContactId'] = $contactsCache[$aItem['ContactUUID']];
324: } else {
325: $contact = collect(
326: (new \Aurora\System\EAV\Query('Aurora\Modules\Contacts\Classes\Contact'))
327: ->where(['UUID' => $aItem['ContactUUID']])
328: ->asArray()
329: ->exec()
330: )->first();
331:
332: $contactsCache[$aItem['ContactUUID']] = $contact['EntityId'];
333: $migrateArray['ContactId'] = $contact['EntityId'];
334: }
335:
336: if (isset($groupsCache[$aItem['GroupUUID']])) {
337: $migrateArray['GroupId'] = $groupsCache[$aItem['GroupUUID']];
338: } else {
339: $group = collect(
340: (new \Aurora\System\EAV\Query('Aurora\Modules\Contacts\Classes\Group'))
341: ->where(['UUID' => $aItem['GroupUUID']])
342: ->asArray()
343: ->exec()
344: )->first();
345: $groupsCache[$aItem['GroupUUID']] = $group['EntityId'];
346: $migrateArray['GroupId'] = $group['EntityId'];
347: }
348: break;
349:
350: default:
351: break;
352: }
353:
354: $properties = $this->getProperties($entity->entity_type, collect($aItem));
355: if ($properties) {
356: $migrateArray['Properties'] = $properties;
357: }
358:
359: if ($entity->entity_type === 'Aurora\Modules\Core\Classes\User' || $entity->entity_type === 'Aurora\Modules\Core\Classes\Tenant') {
360: $oItem = collect(
361: (new \Aurora\System\EAV\Query($entity->entity_type))
362: ->where(['EntityId' => [$entity->id, '=']])
363: ->exec()
364: )->first();
365: $disabledModules = $oItem->getDisabledModules();
366: if ($disabledModules) {
367: $migrateArray['Properties']['DisabledModules'] = implode('|', $disabledModules);
368: }
369: }
370: $newRow = $laravelModel::create(array_merge($aItem, $migrateArray));
371:
372: $this->rewriteFile($fdProgress, $entity->id);
373: $progressBar->advance();
374: } catch (\Illuminate\Database\QueryException $e) {
375: $errorCode = $e->getCode();
376: $errorMessage = $e->getMessage();
377: $shortErrorMessage = $e->errorInfo[2];
378: $missedEntities[$entity->entity_type][] = $entity->id;
379: $missedIds[] = $entity->id;
380: $progressBar->advance();
381: switch ($errorCode) {
382: case 23000:
383: $this->logger->error("Found duplicate for entity with id $entity->id, skipping.");
384: break;
385:
386: default:
387: $this->logger->error($shortErrorMessage);
388: break;
389: }
390: } finally {
391: $this->rewriteFile($fdErrors, $this->jsonPretify($missedEntities));
392: $this->rewriteFile($fdMissedIds, implode(',', $missedIds));
393: }
394: }
395: $this->logger->info('Migration Completed!');
396: DB::statement('SET FOREIGN_KEY_CHECKS=1;');
397: return ['MissedEntities' => $missedEntities, 'MissedIds' => $missedIds];
398: }
399: }
400: