Completed
Push — master ( 926271...fa5020 )
by Thomas
14:41
created

SyncBackend::reEnableUsers()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 21

Duplication

Lines 8
Ratio 38.1 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 2
dl 8
loc 21
rs 9.584
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Jörn Friedrich Dreyer <[email protected]>
4
 * @author Thomas Müller <[email protected]>
5
 *
6
 * @copyright Copyright (c) 2018, ownCloud GmbH
7
 * @license AGPL-3.0
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
23
namespace OC\Core\Command\User;
24
25
use OC\User\Account;
26
use OC\User\AccountMapper;
27
use OC\User\Sync\AllUsersIterator;
28
use OC\User\Sync\SeenUsersIterator;
29
use OC\User\SyncService;
30
use OCP\IConfig;
31
use OCP\ILogger;
32
use OCP\IUser;
33
use OCP\IUserManager;
34
use OCP\UserInterface;
35
use Symfony\Component\Console\Command\Command;
36
use Symfony\Component\Console\Helper\ProgressBar;
37
use Symfony\Component\Console\Question\ChoiceQuestion;
38
use Symfony\Component\Console\Input\InputInterface;
39
use Symfony\Component\Console\Input\InputOption;
40
use Symfony\Component\Console\Output\OutputInterface;
41
use Symfony\Component\Console\Input\InputArgument;
42
43
class SyncBackend extends Command {
44
	const VALID_ACTIONS = ['disable', 'remove'];
45
46
	/** @var AccountMapper */
47
	protected $accountMapper;
48
	/** @var IConfig */
49
	private $config;
50
	/** @var IUserManager */
51
	private $userManager;
52
	/** @var ILogger */
53
	private $logger;
54
55
	/**
56
	 * @param AccountMapper $accountMapper
57
	 * @param IConfig $config
58
	 * @param IUserManager $userManager
59
	 * @param ILogger $logger
60
	 */
61
	public function __construct(AccountMapper $accountMapper,
62
								IConfig $config,
63
								IUserManager $userManager,
64
								ILogger $logger) {
65
		parent::__construct();
66
		$this->accountMapper = $accountMapper;
67
		$this->config = $config;
68
		$this->userManager = $userManager;
69
		$this->logger = $logger;
70
	}
71
72
	protected function configure() {
73
		$this
74
			->setName('user:sync')
75
			->setDescription('Synchronize users from a given backend to the accounts table.')
76
			->addArgument(
77
				'backend-class',
78
				InputArgument::OPTIONAL,
79
				'The PHP class name - e.g., "OCA\User_LDAP\User_Proxy". Please wrap the class name in double quotes. You can use the option --list to list all known backend classes.'
80
			)
81
			->addOption(
82
				'list',
83
				'l',
84
				InputOption::VALUE_NONE,
85
				'List all known backend classes'
86
			)
87
			->addOption(
88
				'uid',
89
				'u',
90
				InputOption::VALUE_REQUIRED,
91
				'sync only the user with the given user id'
92
			)
93
			->addOption(
94
				'seenOnly',
95
				's',
96
				InputOption::VALUE_NONE,
97
				'sync only seen users'
98
			)
99
			->addOption(
100
				'showCount',
101
				'c',
102
				InputOption::VALUE_NONE,
103
				'calculate user count before syncing'
104
			)
105
			->addOption(
106
				'missing-account-action',
107
				'm',
108
				InputOption::VALUE_REQUIRED,
109
				'Action to take if the account isn\'t connected to a backend any longer. Options are "disable" and "remove". Note that removing the account will also remove the stored data and files for that account.'
110
			)
111
			->addOption(
112
				're-enable',
113
				'r',
114
				InputOption::VALUE_NONE,
115
				'When syncing multiple accounts re-enable accounts that are disabled in ownCloud but available in the synced backend.'
116
			);
117
	}
118
119
	/**
120
	 * @param InputInterface $input
121
	 * @param OutputInterface $output
122
	 * @return int|null
123
	 */
124
	protected function execute(InputInterface $input, OutputInterface $output) {
125
		if ($input->getOption('list')) {
126
			$backends = $this->userManager->getBackends();
127
			foreach ($backends as $backend) {
128
				$output->writeln(\get_class($backend));
129
			}
130
			return 0;
131
		}
132
		$backendClassName = $input->getArgument('backend-class');
133
		if ($backendClassName === null) {
134
			$output->writeln('<error>No backend class name given. Please run ./occ help user:sync to understand how this command works.</error>');
135
			return 1;
136
		}
137
		$backend = $this->getBackend($backendClassName);
138
		if ($backend === null) {
139
			$output->writeln("<error>The backend <$backendClassName> does not exist. Did you miss to enable the app?</error>");
140
			return 1;
141
		}
142
		if (!$backend->hasUserListings()) {
143
			$output->writeln("<error>The backend <$backendClassName> does not allow user listing. No sync is possible</error>");
144
			return 1;
145
		}
146
147
		if ($input->getOption('missing-account-action') !== null) {
148
			$missingAccountsAction = $input->getOption('missing-account-action');
149
150
			if (!\in_array($missingAccountsAction, self::VALID_ACTIONS, true)) {
151
				$output->writeln('<error>Unknown action. Choose between "disable" or "remove"</error>');
152
				return 1;
153
			}
154
		} else {
155
			// ask (if possible) how to handle missing accounts. Disable the accounts by default.
156
			$helper = $this->getHelper('question');
157
			$question = new ChoiceQuestion(
158
					'If unknown users are found, what do you want to do with their accounts? (removing the account will also remove its data)',
159
160
					\array_merge(self::VALID_ACTIONS, ['ask later']),
161
					0
162
			);
163
			$missingAccountsAction = $helper->ask($input, $output, $question);
164
		}
165
166
		$syncService = new SyncService($this->config, $this->logger, $this->accountMapper);
167
168
		$uid = $input->getOption('uid');
169
170
		if ($uid) {
171
			$this->syncSingleUser($input, $output, $syncService, $backend, $uid, $missingAccountsAction);
172
		} else {
173
			$this->syncMultipleUsers($input, $output, $syncService, $backend, $missingAccountsAction);
174
		}
175
		return 0;
176
	}
177
178
	/**
179
	 * @param InputInterface $input
180
	 * @param OutputInterface $output
181
	 * @param SyncService $syncService
182
	 * @param UserInterface $backend
183
	 * @param string $missingAccountsAction
184
	 */
185
	private function syncMultipleUsers(
186
		InputInterface $input,
187
		OutputInterface $output,
188
		SyncService $syncService,
189
		UserInterface $backend,
190
		$missingAccountsAction
191
	) {
192
		$output->writeln('Analysing known accounts ...');
193
		$p = new ProgressBar($output);
194
		list($removedUsers, $reappearedUsers) = $syncService->analyzeExistingUsers($backend, function () use ($p) {
195
			$p->advance();
196
		});
197
		$p->finish();
198
		$output->writeln('');
199
		$output->writeln('');
200
201
		$this->handleRemovedUsers($removedUsers, $input, $output, $missingAccountsAction);
202
203
		$output->writeln('');
204
205
		if ($input->getOption('re-enable')) {
206
			$this->reEnableUsers($reappearedUsers, $output);
207
		}
208
209
		$output->writeln('');
210
		$backendClass = \get_class($backend);
211
		if ($input->getOption('seenOnly')) {
212
			$output->writeln("Updating seen accounts from $backendClass ...");
213
			$iterator = new SeenUsersIterator($this->accountMapper, $backendClass);
214
		} else {
215
			$output->writeln("Inserting new and updating all known users from $backendClass ...");
216
			$iterator = new AllUsersIterator($backend);
217
		}
218
219
		$p = new ProgressBar($output);
220
		$max = null;
221
		if ($backend->implementsActions(\OC_User_Backend::COUNT_USERS) && $input->getOption('showCount')) {
222
			$max = $backend->countUsers();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\UserInterface as the method countUsers() does only exist in the following implementations of said interface: OCA\Testing\AlternativeHomeUserBackend, OC\User\Database.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
223
		}
224
		$p->start($max);
225
226
		$syncService->run($backend, $iterator, function () use ($p) {
227
			$p->advance();
228
		});
229
230
		$p->finish();
231
		$output->writeln('');
232
		$output->writeln('');
233
	}
234
235
	/**
236
	 * @param InputInterface $input
237
	 * @param OutputInterface $output
238
	 * @param SyncService $syncService
239
	 * @param UserInterface $backend
240
	 * @param string $uid
241
	 * @param string $missingAccountsAction
242
	 * @throws \LengthException
243
	 */
244
	private function syncSingleUser(
245
		InputInterface $input,
246
		OutputInterface $output,
247
		SyncService $syncService,
248
		UserInterface $backend,
249
		$uid,
250
		$missingAccountsAction
251
	) {
252
		$output->writeln("Syncing $uid ...");
253
		$users = $backend->getUsers($uid, 2);
254
255
		if (\count($users) > 1) {
256
			throw new \LengthException("Multiple users returned from backend for: $uid. Cancelling sync.");
257
		}
258
259
		$dummy = new Account(); // to prevent null pointer when writing messages
260
		if (\count($users) === 1) {
261
			// Run the sync using the internal username if mapped
262
			$syncService->run($backend, new \ArrayIterator([$users[0]]), function () {
263
			});
264
		} else {
265
			// Not found
266
			$this->handleRemovedUsers([$uid => $dummy], $input, $output, $missingAccountsAction);
0 ignored issues
show
Documentation introduced by
array($uid => $dummy) is of type array<string,object<OC\User\Account>>, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
267
		}
268
269
		$output->writeln('');
270
271
		if ($input->getOption('re-enable')) {
272
			$this->reEnableUsers([$uid => $dummy], $output);
273
		}
274
	}
275
	/**
276
	 * @param $backend
277
	 * @return null|UserInterface
278
	 */
279
	private function getBackend($backend) {
280
		$backends = $this->userManager->getBackends();
281
		$match = \array_filter($backends, function ($b) use ($backend) {
282
			return \get_class($b) === $backend;
283
		});
284
		if (empty($match)) {
285
			return null;
286
		}
287
		return \array_pop($match);
288
	}
289
290
	/**
291
	 * @param array $uidToAccountMap a list of uids to account objects
292
	 * @param callable $callbackExists the callback used if the account for the uid exists. The
293
	 * uid and the specific account will be passed as parameter to the callback in that order
294
	 * @param callable $callbackMissing the callback used if the account doesn't exists.
295
	 * The uid and account are passed as parameters to the callback
296
	 */
297
	private function doActionForAccountUids(array $uidToAccountMap, callable $callbackExists, callable $callbackMissing = null) {
298
		foreach ($uidToAccountMap as $uid => $account) {
299
			$user = $this->userManager->get($uid);
300
			if ($user === null) {
301
				$callbackMissing($uid, $account);
302
			} else {
303
				$callbackExists($uid, $user);
304
			}
305
		}
306
	}
307
308
	/**
309
	 * @param string[] $removedUsers
310
	 * @param InputInterface $input
311
	 * @param OutputInterface $output
312
	 * @param $missingAccountsAction
313
	 */
314
	private function handleRemovedUsers(array $removedUsers, InputInterface $input, OutputInterface $output, $missingAccountsAction) {
315
		if (empty($removedUsers)) {
316
			$output->writeln('No removed users have been detected.');
317
		} else {
318
319
			// define some actions to be used
320 View Code Duplication
			$disableAction = function ($uid, IUser $user) use ($output) {
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...
321
				if ($user->isEnabled()) {
322
					$user->setEnabled(false);
323
					$output->writeln("$uid, {$user->getDisplayName()}, {$user->getEMailAddress()} disabled");
324
				} else {
325
					$output->writeln("$uid, {$user->getDisplayName()}, {$user->getEMailAddress()} skipped, already disabled");
326
				}
327
			};
328
			$deleteAction = function ($uid, IUser $user) use ($output) {
329
				$user->delete();
330
				$output->writeln("$uid, {$user->getDisplayName()}, {$user->getEMailAddress()} deleted");
331
			};
332
			$writeNotExisting = function ($uid, Account $account) use ($output) {
333
				$output->writeln("$uid, {$account->getDisplayName()}, {$account->getEmail()} (no longer exists in the backend)");
334
			};
335
336
			switch ($missingAccountsAction) {
337
				case 'disable':
338
					$output->writeln('Disabling accounts:');
339
					$this->doActionForAccountUids(
340
						$removedUsers,
341
						$disableAction,
342
						$writeNotExisting
343
					);
344
					break;
345
				case 'remove':
346
					$output->writeln('Deleting accounts:');
347
					$this->doActionForAccountUids(
348
						$removedUsers,
349
						$deleteAction,
350
						$writeNotExisting
351
					);
352
					break;
353
				case 'ask later':
354
					$output->writeln('These accounts that are no longer available in the backend:');
355
					$this->doActionForAccountUids(
356
						$removedUsers,
357
						function ($uid) use ($output) {
358
							$output->writeln($uid);
359
						},
360
						$writeNotExisting
361
					);
362
363
					$helper = $this->getHelper('question');
364
					$question = new ChoiceQuestion(
365
						'What do you want to do with their accounts? (removing the account will also remove its data)',
366
						self::VALID_ACTIONS,
367
						0
368
					);
369
					$missingAccountsAction2 = $helper->ask($input, $output, $question);
370
					switch ($missingAccountsAction2) {
371
						// if "nothing" is selected, just ignore and finish
372
						case 'disable':
373
							$output->writeln('Disabling accounts');
374
							$this->doActionForAccountUids(
375
								$removedUsers,
376
								$disableAction,
377
								$writeNotExisting
378
							);
379
							break;
380
						case 'remove':
381
							$output->writeln('Deleting accounts:');
382
							$this->doActionForAccountUids(
383
								$removedUsers,
384
								$deleteAction,
385
								$writeNotExisting
386
							);
387
							break;
388
					}
389
					break;
390
			}
391
		}
392
	}
393
394
	/**
395
	 * Re-enable disabled accounts
396
	 * @param array $reappearedUsers map of uids to account objects
397
	 * @param OutputInterface $output
398
	 */
399
	private function reEnableUsers(array $reappearedUsers, OutputInterface $output) {
400
		if (empty($reappearedUsers)) {
401
			$output->writeln('No existing accounts to re-enable.');
402
		} else {
403
			$output->writeln('Re-enabling accounts:');
404
405
			$this->doActionForAccountUids($reappearedUsers,
406 View Code Duplication
				function ($uid, IUser $user) use ($output) {
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...
407
					if ($user->isEnabled()) {
408
						$output->writeln("$uid, {$user->getDisplayName()}, {$user->getEMailAddress()} skipped, already enabled");
409
					} else {
410
						$user->setEnabled(true);
411
						$output->writeln("$uid, {$user->getDisplayName()}, {$user->getEMailAddress()} enabled");
412
					}
413
				},
414
				function ($uid) use ($output) {
415
					$output->writeln("$uid not enabled (no existing account found)");
416
				}
417
			);
418
		}
419
	}
420
}
421