Completed
Push — master ( c821ba...1bc6e3 )
by Thomas
21:25
created

SyncBackend::doActionForAccountUids()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 3
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Thomas Müller <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2017, ownCloud GmbH
6
 * @license AGPL-3.0
7
 *
8
 * This code is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License, version 3,
10
 * as published by the Free Software Foundation.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License, version 3,
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19
 *
20
 */
21
22
namespace OC\Core\Command\User;
23
24
25
use OC\User\AccountMapper;
26
use OC\User\SyncService;
27
use OCP\IConfig;
28
use OCP\ILogger;
29
use OCP\IUser;
30
use OCP\IUserManager;
31
use OCP\UserInterface;
32
use Symfony\Component\Console\Command\Command;
33
use Symfony\Component\Console\Helper\ProgressBar;
34
use Symfony\Component\Console\Question\ChoiceQuestion;
35
use Symfony\Component\Console\Input\InputInterface;
36
use Symfony\Component\Console\Input\InputOption;
37
use Symfony\Component\Console\Output\OutputInterface;
38
use Symfony\Component\Console\Input\InputArgument;
39
40
class SyncBackend extends Command {
41
42
	/** @var AccountMapper */
43
	protected $accountMapper;
44
	/** @var IConfig */
45
	private $config;
46
	/** @var IUserManager */
47
	private $userManager;
48
	/** @var ILogger */
49
	private $logger;
50
51
	/**
52
	 * @param AccountMapper $accountMapper
53
	 * @param IConfig $config
54
	 * @param IUserManager $userManager
55
	 * @param ILogger $logger
56
	 */
57
	public function __construct(AccountMapper $accountMapper,
58
								IConfig $config,
59
								IUserManager $userManager,
60
								ILogger $logger) {
61
		parent::__construct();
62
		$this->accountMapper = $accountMapper;
63
		$this->config = $config;
64
		$this->userManager = $userManager;
65
		$this->logger = $logger;
66
	}
67
68
	protected function configure() {
69
		$this
70
			->setName('user:sync')
71
			->setDescription('synchronize users from a given backend to the accounts table')
72
			->addArgument(
73
				'backend-class',
74
				InputArgument::OPTIONAL,
75
				'The php class name - e.g. "OCA\User_LDAP\User_LDAP". Please wrap the class name into double quotes. You can use the option --list to list all known backend classes'
76
			)
77
			->addOption('list', 'l', InputOption::VALUE_NONE, 'list all known backend classes')
78
			->addOption('missing-account-action', 'm', InputOption::VALUE_REQUIRED, 'action to do if the account isn\'t connected to a backend any longer. Options are "disable" and "remove". Use quotes. Note that removing the account will also remove the stored data and files for that account');
79
	}
80
81
	protected function execute(InputInterface $input, OutputInterface $output) {
82
		if ($input->getOption('list')) {
83
			$backends = $this->userManager->getBackends();
84
			foreach ($backends as $backend) {
85
				$output->writeln(get_class($backend));
86
			}
87
			return 0;
88
		}
89
		$backendClassName = $input->getArgument('backend-class');
90
		if (is_null($backendClassName)) {
91
			$output->writeln("<error>No backend class name given. Please run ./occ help user:sync to understand how this command works.</error>");
92
			return 1;
93
		}
94
		$backend = $this->getBackend($backendClassName);
95
		if (is_null($backend)) {
96
			$output->writeln("<error>The backend <$backendClassName> does not exist. Did you miss to enable the app?</error>");
97
			return 1;
98
		}
99
		if (!$backend->hasUserListings()) {
100
			$output->writeln("<error>The backend <$backendClassName> does not allow user listing. No sync is possible</error>");
101
			return 1;
102
		}
103
104
		$validActions = ['disable', 'remove'];
105
106
		if ($input->getOption('missing-account-action') !== null) {
107
			$missingAccountsAction = $input->getOption('missing-account-action');
108
			if (!in_array($missingAccountsAction, $validActions, true)) {
109
				$output->writeln("<error>Unknown action. Choose between \"disable\" or \"remove\"</error>");
110
				return 1;
111
			}
112
		} else {
113
			// ask (if possible) how to handle missing accounts. Disable the accounts by default.
114
			$helper = $this->getHelper('question');
115
			$question = new ChoiceQuestion(
116
					'If unknown users are found, what do you want to do with their accounts? (removing the account will also remove its data)',
117
					array_merge($validActions, ['ask later']),
118
					0
119
			);
120
			$missingAccountsAction = $helper->ask($input, $output, $question);
121
		}
122
123
		$syncService = new SyncService($this->accountMapper, $backend, $this->config, $this->logger);
124
125
		// insert/update known users
126
		$output->writeln("Insert new and update existing users ...");
127
		$p = new ProgressBar($output);
128
		$max = null;
129
		if ($backend->implementsActions(\OC_User_Backend::COUNT_USERS)) {
130
			$max = $backend->countUsers();
131
		}
132
		$p->start($max);
133
		$syncService->run(function () use ($p) {
134
			$p->advance();
135
		});
136
		$p->finish();
137
		$output->writeln('');
138
		$output->writeln('');
139
140
		// analyse unknown users
141
		$this->handleUnknownUsers($input, $output, $syncService, $missingAccountsAction, $validActions);
142
143
		return 0;
144
	}
145
146
	/**
147
	 * @param $backend
148
	 * @return null|UserInterface
149
	 */
150
	private function getBackend($backend) {
151
		$backends = $this->userManager->getBackends();
152
		$match = array_filter($backends, function ($b) use ($backend) {
153
			return get_class($b) === $backend;
154
		});
155
		if (empty($match)) {
156
			return null;
157
		}
158
		return array_pop($match);
159
	}
160
161
	/**
162
	 * @param array $uids a list of uids to the the action
163
	 * @param callable $callbackExists the callback used if the account for the uid exists. The
164
	 * uid and the specific account will be passed as parameter to the callback in that order
165
	 * @param callable $callbackMissing the callback used if the account doesn't exists. The uid (not
166
	 * the account) will be passed as parameter to the callback
167
	 */
168
	private function doActionForAccountUids(array $uids, callable $callbackExists, callable $callbackMissing = null) {
169
		foreach ($uids as $u) {
170
			$userAccount = $this->userManager->get($u);
171
			if ($userAccount === null) {
172
				$callbackMissing($u);
173
			} else {
174
				$callbackExists($u, $userAccount);
175
			}
176
		}
177
	}
178
179
	/**
180
	 * @param InputInterface $input
181
	 * @param OutputInterface $output
182
	 * @param $syncService
183
	 * @param $missingAccountsAction
184
	 * @param $validActions
185
	 */
186
	private function handleUnknownUsers(InputInterface $input, OutputInterface $output, $syncService, $missingAccountsAction, $validActions) {
187
		$output->writeln("Analyse unknown users ...");
188
		$p = new ProgressBar($output);
189
		$toBeDeleted = $syncService->getNoLongerExistingUsers(function () use ($p) {
190
			$p->advance();
191
		});
192
		$p->finish();
193
		$output->writeln('');
194
		$output->writeln('');
195
196
		if (empty($toBeDeleted)) {
197
			$output->writeln("No unknown users have been detected.");
198
		} else {
199
			$output->writeln("Following users are no longer known with the connected backend.");
200
			switch ($missingAccountsAction) {
201 View Code Duplication
				case 'disable':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
					$output->writeln("Proceeding to disable the accounts");
203
					$this->doActionForAccountUids($toBeDeleted,
204
						function ($uid, IUser $ac) use ($output) {
205
							$ac->setEnabled(false);
206
							$output->writeln($uid);
207
						},
208
						function ($uid) use ($output) {
209
							$output->writeln($uid . " (unknown account for the user)");
210
						});
211
					break;
212 View Code Duplication
				case 'remove':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
					$output->writeln("Proceeding to remove the accounts");
214
					$this->doActionForAccountUids($toBeDeleted,
215
						function ($uid, IUser $ac) use ($output) {
216
							$ac->delete();
217
							$output->writeln($uid);
218
						},
219
						function ($uid) use ($output) {
220
							$output->writeln($uid . " (unknown account for the user)");
221
						});
222
					break;
223
				case 'ask later':
224
					$output->writeln("listing the unknown accounts");
225
					$this->doActionForAccountUids($toBeDeleted,
226
						function ($uid) use ($output) {
227
							$output->writeln($uid);
228
						},
229
						function ($uid) use ($output) {
230
							$output->writeln($uid . " (unknown account for the user)");
231
						});
232
					// overwriting variables!
233
					$helper = $this->getHelper('question');
234
					$question = new ChoiceQuestion(
235
						'What do you want to do with their accounts? (removing the account will also remove its data)',
236
						$validActions,
237
						0
238
					);
239
					$missingAccountsAction2 = $helper->ask($input, $output, $question);
240
					switch ($missingAccountsAction2) {
241
						// if "nothing" is selected, just ignore and finish
242 View Code Duplication
						case 'disable':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
							$output->writeln("Proceeding to disable the accounts");
244
							$this->doActionForAccountUids($toBeDeleted,
245
								function ($uid, IUser $ac) {
246
									$ac->setEnabled(false);
247
								},
248
								function ($uid) use ($output) {
249
									$output->writeln($uid . " (unknown account for the user)");
250
								});
251
							break;
252 View Code Duplication
						case 'remove':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
253
							$output->writeln("Proceeding to remove the accounts");
254
							$this->doActionForAccountUids($toBeDeleted,
255
								function ($uid, IUser $ac) {
256
									$ac->delete();
257
								},
258
								function ($uid) use ($output) {
259
									$output->writeln($uid . " (unknown account for the user)");
260
								});
261
							break;
262
					}
263
					break;
264
			}
265
		}
266
	}
267
}
268