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: * @var ConsoleLogger
20: */
21: private $logger = false;
22:
23: /**
24: * @return void
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(); // get primary key column name
88:
89: // This block is required for the work with custom connection to DB which is defined in MtaConnector module
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: