1: | <?php |
2: | |
3: | namespace Aurora\System\Console\Commands; |
4: | |
5: | use Aurora\System\Api; |
6: | use Aurora\System\Console\Commands\BaseCommand; |
7: | use Symfony\Component\Console\Command\Command; |
8: | use Symfony\Component\Console\Input\InputInterface; |
9: | use Symfony\Component\Console\Input\InputOption; |
10: | use Symfony\Component\Console\Logger\ConsoleLogger; |
11: | use Symfony\Component\Console\Output\OutputInterface; |
12: | use Symfony\Component\Console\Question\ConfirmationQuestion; |
13: | use Illuminate\Database\Capsule\Manager as Capsule; |
14: | use Psr\Log\LogLevel; |
15: | |
16: | class OrphansCommand extends BaseCommand |
17: | { |
18: | |
19: | |
20: | |
21: | private $logger = false; |
22: | |
23: | |
24: | |
25: | |
26: | public function __construct() |
27: | { |
28: | parent::__construct(); |
29: | } |
30: | |
31: | protected function configure(): void |
32: | { |
33: | $this->setName('orphans') |
34: | ->setDescription('Collect orphan entries') |
35: | ->addOption('remove', 'r', InputOption::VALUE_NONE, 'Remove orphan entries from DB.') |
36: | ; |
37: | } |
38: | |
39: | protected function rewriteFile($fd, $str) |
40: | { |
41: | ftruncate($fd, 0); |
42: | fseek($fd, 0, SEEK_END); |
43: | fwrite($fd, $str); |
44: | } |
45: | |
46: | protected function jsonPretify($sJsonStr) |
47: | { |
48: | $sOutput = '{'; |
49: | $bFirstElement = true; |
50: | foreach ($sJsonStr as $key => $value) { |
51: | if (!$bFirstElement) { |
52: | $sOutput .= ","; |
53: | } |
54: | $bFirstElement = false; |
55: | |
56: | $sOutput .= PHP_EOL . "\t\"" . $key . "\": ["; |
57: | $sOutput .= PHP_EOL . "\t\t\"" . implode('","', $value) . "\""; |
58: | $sOutput .= PHP_EOL . "\t]"; |
59: | } |
60: | $sOutput .= PHP_EOL . '}'; |
61: | $sOutput = str_replace('\\', '\\\\', $sOutput); |
62: | |
63: | return $sOutput; |
64: | } |
65: | |
66: | protected function checkOrphans($fdEntities, $input, $output) |
67: | { |
68: | $helper = $this->getHelper('question'); |
69: | $question = new ConfirmationQuestion('Remove these orphan entries? [yes]', true); |
70: | |
71: | $aOrphansEntities = []; |
72: | $aModels = $this->getAllModels(); |
73: | foreach ($aModels as $moduleName => $moduleModels) { |
74: | foreach ($moduleModels as $modelPath) { |
75: | $model = str_replace('/', DIRECTORY_SEPARATOR, $modelPath); |
76: | $model = str_replace('\\', DIRECTORY_SEPARATOR, $model); |
77: | $model = explode(DIRECTORY_SEPARATOR, $model); |
78: | |
79: | while ($model[0] !== 'modules') { |
80: | array_shift($model); |
81: | } |
82: | $model[0] = 'Modules'; |
83: | array_unshift($model, "Aurora"); |
84: | $model = implode('\\', $model); |
85: | |
86: | $modelObject = new $model(); |
87: | $primaryKey = $modelObject->getKeyName(); |
88: | |
89: | |
90: | $sConnectionName = $modelObject->getConnectionName(); |
91: | if ($sConnectionName) { |
92: | $sModuleClassName = '\\Aurora\\Modules\\' . $moduleName . '\\Module'; |
93: | $sModelPath = Api::GetModuleManager()->GetModulePath($moduleName); |
94: | $oModule = new $sModuleClassName($sModelPath); |
95: | $oModule->addDbConnection(); |
96: | } |
97: | |
98: | $this->logger->info('Checking ' . $model::query()->getQuery()->from . ' table.'); |
99: | |
100: | $checkOrphan = $modelObject->getOrphanIds(); |
101: | switch($checkOrphan['status']) { |
102: | case 0: |
103: | $this->logger->info($checkOrphan['message']); |
104: | break; |
105: | case 1: |
106: | $aOrphansEntities[$model] = array_values($checkOrphan['orphansIds']); |
107: | sort($aOrphansEntities[$model]); |
108: | if ($input->getOption('remove') && !empty($aOrphansEntities[$model])) { |
109: | $this->logger->error($checkOrphan['message']); |
110: | $bRemove = $helper->ask($input, $output, $question); |
111: | |
112: | if ($bRemove) { |
113: | $modelObject::whereIn($primaryKey, $aOrphansEntities[$model])->delete(); |
114: | $this->logger->warning('Orphan entries was removed.'); |
115: | } else { |
116: | $this->logger->warning('Orphan entries removing was skipped.'); |
117: | } |
118: | } else { |
119: | $this->logger->error($checkOrphan['message']); |
120: | } |
121: | break; |
122: | default: |
123: | $this->logger->info($checkOrphan['message']); |
124: | break; |
125: | } |
126: | echo PHP_EOL; |
127: | } |
128: | } |
129: | return $aOrphansEntities; |
130: | } |
131: | |
132: | protected function checkFileOrphans($fdEntities, $input, $output) |
133: | { |
134: | $helper = $this->getHelper('question'); |
135: | $question = new ConfirmationQuestion('Remove files of the orphan users? [yes]', true); |
136: | |
137: | $dirFiles = \Aurora\System\Api::DataPath() . "/files"; |
138: | $dirPersonalFiles = $dirFiles . "/private"; |
139: | $dirOrphanFiles = $dirFiles . "/orphan_user_files"; |
140: | $aOrphansEntities = []; |
141: | |
142: | if (is_dir($dirPersonalFiles)) { |
143: | $this->logger->info("Checking Personal files."); |
144: | |
145: | $dirs = array_diff(scandir($dirPersonalFiles), array('..', '.')); |
146: | |
147: | $orphanUUIDs = array_values(array_diff($dirs, \Aurora\Modules\Core\Models\User::query()->pluck('UUID')->toArray())); |
148: | |
149: | if (!empty($orphanUUIDs)) { |
150: | $aOrphansEntities['PersonalFiles'] = $orphanUUIDs; |
151: | |
152: | $this->logger->error("Personal files orphans were found: " . count($orphanUUIDs)); |
153: | |
154: | if ($input->getOption('remove')) { |
155: | $bRemove = $helper->ask($input, $output, $question); |
156: | |
157: | if ($bRemove) { |
158: | if (!is_dir($dirOrphanFiles)) { |
159: | mkdir($dirOrphanFiles); |
160: | } |
161: | |
162: | foreach ($orphanUUIDs as $orphanUUID) { |
163: | rename($dirPersonalFiles . "/" . $orphanUUID, $dirOrphanFiles . "/" . $orphanUUID); |
164: | } |
165: | |
166: | $this->logger->warning('Orphan user files were moved to ' . $dirOrphanFiles . '.'); |
167: | } else { |
168: | $this->logger->warning('Orphan user files removing was skipped.'); |
169: | } |
170: | } |
171: | } else { |
172: | $this->logger->info("Personal files orphans were not found."); |
173: | } |
174: | } |
175: | return $aOrphansEntities; |
176: | } |
177: | |
178: | protected function checkDavOrphans($fdEntities, $input, $output) |
179: | { |
180: | $helper = $this->getHelper('question'); |
181: | $question = new ConfirmationQuestion('Remove DAV objects of the orphan users? [yes]', true); |
182: | |
183: | $dbPrefix = Api::GetSettings()->DBPrefix; |
184: | $aOrphansEntities = []; |
185: | if (Capsule::schema()->hasTable('adav_calendarinstances')) { |
186: | echo PHP_EOL; |
187: | $this->logger->info("Checking DAV calendar."); |
188: | |
189: | $rows = Capsule::connection()->select('SELECT aci.calendarid, aci.id FROM ' . $dbPrefix . 'adav_calendarinstances as aci |
190: | WHERE SUBSTRING(principaluri, 12) NOT IN (SELECT PublicId FROM ' . $dbPrefix . 'core_users) AND principaluri NOT LIKE \'%_dav_tenant_user@%\''); |
191: | |
192: | if (count($rows) > 0) { |
193: | $this->logger->error("DAV calendars orphans were found: " . count($rows)); |
194: | |
195: | $aOrphansEntities['DAV-Calendars'] = array_map(function ($row) { |
196: | return $row->id; |
197: | }, $rows); |
198: | |
199: | if ($input->getOption('remove')) { |
200: | $bRemove = $helper->ask($input, $output, $question); |
201: | |
202: | if ($bRemove) { |
203: | foreach ($rows as $row) { |
204: | \Afterlogic\DAV\Backend::Caldav()->deleteCalendar([$row->calendarid, $row->id]); |
205: | } |
206: | } |
207: | } |
208: | } else { |
209: | $this->logger->info("DAV calendars orphans were not found."); |
210: | } |
211: | } |
212: | |
213: | if (Capsule::schema()->hasTable('adav_addressbooks')) { |
214: | echo PHP_EOL; |
215: | $this->logger->info("Checking DAV addressbooks."); |
216: | |
217: | $rows = Capsule::connection()->select('SELECT id FROM ' . $dbPrefix . 'adav_addressbooks WHERE (SUBSTRING(principaluri, 12) NOT IN (SELECT PublicId FROM ' . $dbPrefix . 'core_users) AND principaluri NOT LIKE \'%_dav_tenant_user@%\') OR ISNULL(principaluri)'); |
218: | |
219: | if (count($rows) > 0) { |
220: | $this->logger->error("DAV addressbooks orphans were found: " . count($rows)); |
221: | |
222: | $aOrphansEntities['DAV-Addressbooks'] = array_map(function ($row) { |
223: | return $row->id; |
224: | }, $rows); |
225: | |
226: | if ($input->getOption('remove')) { |
227: | $bRemove = $helper->ask($input, $output, $question); |
228: | |
229: | if ($bRemove) { |
230: | foreach ($rows as $row) { |
231: | \Afterlogic\DAV\Backend::Carddav()->deleteAddressBook($row->id); |
232: | } |
233: | } |
234: | } |
235: | } else { |
236: | $this->logger->info("DAV addressbooks orphans were not found."); |
237: | } |
238: | } |
239: | |
240: | return $aOrphansEntities; |
241: | } |
242: | |
243: | protected function execute(InputInterface $input, OutputInterface $output): int |
244: | { |
245: | $verbosityLevelMap = [ |
246: | LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, |
247: | LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL, |
248: | ]; |
249: | |
250: | $dirName = \Aurora\System\Logger::GetLogFileDir() . "/orphans-logs"; |
251: | $entitiesFileName = $dirName . "/orphans_" . date('Y-m-d_H-i-s') . ".json"; |
252: | $orphansEntities = []; |
253: | |
254: | $dirname = dirname($entitiesFileName); |
255: | if (!is_dir($dirname)) { |
256: | mkdir($dirname, 0755, true); |
257: | } |
258: | |
259: | $fdEntities = fopen($entitiesFileName, 'a+') or die("Can't create migration-progress.txt file"); |
260: | |
261: | $this->logger = new ConsoleLogger($output, $verbosityLevelMap); |
262: | $orphansEntities = array_merge( |
263: | $orphansEntities, |
264: | $this->checkOrphans($fdEntities, $input, $output) |
265: | ); |
266: | $orphansEntities = array_merge( |
267: | $orphansEntities, |
268: | $this->checkFileOrphans($fdEntities, $input, $output) |
269: | ); |
270: | $orphansEntities = array_merge( |
271: | $orphansEntities, |
272: | $this->checkDavOrphans($fdEntities, $input, $output) |
273: | ); |
274: | |
275: | if (count($orphansEntities) > 0) { |
276: | $this->rewriteFile($fdEntities, $this->jsonPretify($orphansEntities)); |
277: | } |
278: | |
279: | return Command::SUCCESS; |
280: | } |
281: | } |
282: | |