Passed
Push — master ( 8e9af2...a5c558 )
by John
14:03 queued 11s
created

MoveCalendar::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 0
dl 0
loc 14
rs 9.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 *
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Georg Ehrke <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Roeland Jago Douma <[email protected]>
9
 * @author Thomas Citharel <[email protected]>
10
 *
11
 * @license GNU AGPL version 3 or any later version
12
 *
13
 * This program is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License as
15
 * published by the Free Software Foundation, either version 3 of the
16
 * License, or (at your option) any later version.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License
24
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
 *
26
 */
27
28
namespace OCA\DAV\Command;
29
30
use OCA\DAV\CalDAV\CalDavBackend;
31
use OCA\DAV\CalDAV\Calendar;
32
use OCP\IConfig;
33
use OCP\IGroupManager;
34
use OCP\IL10N;
35
use OCP\IUserManager;
36
use OCP\Share\IManager as IShareManager;
37
use Symfony\Component\Console\Command\Command;
38
use Symfony\Component\Console\Input\InputArgument;
39
use Symfony\Component\Console\Input\InputInterface;
40
use Symfony\Component\Console\Input\InputOption;
41
use Symfony\Component\Console\Output\OutputInterface;
42
use Symfony\Component\Console\Style\SymfonyStyle;
43
44
class MoveCalendar extends Command {
45
46
	/** @var IUserManager */
47
	private $userManager;
48
49
	/** @var IGroupManager */
50
	private $groupManager;
51
52
	/** @var IShareManager */
53
	private $shareManager;
54
55
	/** @var IConfig $config */
56
	private $config;
57
58
	/** @var IL10N */
59
	private $l10n;
60
61
	/** @var SymfonyStyle */
62
	private $io;
63
64
	/** @var CalDavBackend */
65
	private $calDav;
66
67
	public const URI_USERS = 'principals/users/';
68
69
	/**
70
	 * @param IUserManager $userManager
71
	 * @param IGroupManager $groupManager
72
	 * @param IShareManager $shareManager
73
	 * @param IConfig $config
74
	 * @param IL10N $l10n
75
	 * @param CalDavBackend $calDav
76
	 */
77
	public function __construct(
78
		IUserManager $userManager,
79
		IGroupManager $groupManager,
80
		IShareManager $shareManager,
81
		IConfig $config,
82
		IL10N $l10n,
83
		CalDavBackend $calDav
84
	) {
85
		parent::__construct();
86
		$this->userManager = $userManager;
87
		$this->groupManager = $groupManager;
88
		$this->shareManager = $shareManager;
89
		$this->config = $config;
90
		$this->l10n = $l10n;
91
		$this->calDav = $calDav;
92
	}
93
94
	protected function configure() {
95
		$this
96
			->setName('dav:move-calendar')
97
			->setDescription('Move a calendar from an user to another')
98
			->addArgument('name',
99
				InputArgument::REQUIRED,
100
				'Name of the calendar to move')
101
			->addArgument('sourceuid',
102
				InputArgument::REQUIRED,
103
				'User who currently owns the calendar')
104
			->addArgument('destinationuid',
105
				InputArgument::REQUIRED,
106
				'User who will receive the calendar')
107
			->addOption('force', 'f', InputOption::VALUE_NONE, "Force the migration by removing existing shares and renaming calendars in case of conflicts");
108
	}
109
110
	protected function execute(InputInterface $input, OutputInterface $output): int {
111
		$userOrigin = $input->getArgument('sourceuid');
112
		$userDestination = $input->getArgument('destinationuid');
113
114
		$this->io = new SymfonyStyle($input, $output);
115
116
		if (!$this->userManager->userExists($userOrigin)) {
117
			throw new \InvalidArgumentException("User <$userOrigin> is unknown.");
118
		}
119
120
		if (!$this->userManager->userExists($userDestination)) {
121
			throw new \InvalidArgumentException("User <$userDestination> is unknown.");
122
		}
123
124
		$name = $input->getArgument('name');
125
		$newName = null;
126
127
		$calendar = $this->calDav->getCalendarByUri(self::URI_USERS . $userOrigin, $name);
0 ignored issues
show
Bug introduced by
Are you sure $userOrigin of type null|string|string[] can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

127
		$calendar = $this->calDav->getCalendarByUri(self::URI_USERS . /** @scrutinizer ignore-type */ $userOrigin, $name);
Loading history...
Bug introduced by
Are you sure the assignment to $calendar is correct as $this->calDav->getCalend...S . $userOrigin, $name) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarByUri() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
128
129
		if (null === $calendar) {
0 ignored issues
show
introduced by
The condition null === $calendar is always true.
Loading history...
130
			throw new \InvalidArgumentException("User <$userOrigin> has no calendar named <$name>. You can run occ dav:list-calendars to list calendars URIs for this user.");
131
		}
132
133
		// Calendar already exists
134
		if ($this->calendarExists($userDestination, $name)) {
135
			if ($input->getOption('force')) {
136
				// Try to find a suitable name
137
				$newName = $this->getNewCalendarName($userDestination, $name);
138
139
				// If we didn't find a suitable value after all the iterations, give up
140
				if ($this->calendarExists($userDestination, $newName)) {
141
					throw new \InvalidArgumentException("Unable to find a suitable calendar name for <$userDestination> with initial name <$name>.");
142
				}
143
			} else {
144
				throw new \InvalidArgumentException("User <$userDestination> already has a calendar named <$name>.");
145
			}
146
		}
147
148
		$hadShares = $this->checkShares($calendar, $userOrigin, $userDestination, $input->getOption('force'));
149
		if ($hadShares) {
150
			/**
151
			 * Warn that share links have changed if there are shares
152
			 */
153
			$this->io->note([
154
				"Please note that moving calendar " . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.",
155
				"Sharees will need to change \"example.com/remote.php/dav/calendars/uid/" . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\""
156
			]);
157
		}
158
159
		$this->calDav->moveCalendar($name, self::URI_USERS . $userOrigin, self::URI_USERS . $userDestination, $newName);
160
161
		$this->io->success("Calendar <$name> was moved from user <$userOrigin> to <$userDestination>" . ($newName ? " as <$newName>" : ''));
162
		return 0;
163
	}
164
165
	/**
166
	 * Check if the calendar exists for user
167
	 *
168
	 * @param string $userDestination
169
	 * @param string $name
170
	 * @return bool
171
	 */
172
	protected function calendarExists(string $userDestination, string $name): bool {
173
		return null !== $this->calDav->getCalendarByUri(self::URI_USERS . $userDestination, $name);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->calDav->getCalend...userDestination, $name) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarByUri() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
174
	}
175
176
	/**
177
	 * Try to find a suitable new calendar name that
178
	 * doesn't exists for the provided user
179
	 *
180
	 * @param string $userDestination
181
	 * @param string $name
182
	 * @return string
183
	 */
184
	protected function getNewCalendarName(string $userDestination, string $name): string {
185
		$increment = 1;
186
		$newName = $name . '-' . $increment;
187
		while ($increment <= 10) {
188
			$this->io->writeln("Trying calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE);
189
			if (!$this->calendarExists($userDestination, $newName)) {
190
				// New name is good to go
191
				$this->io->writeln("Found proper new calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE);
192
				break;
193
			}
194
			$newName = $name . '-' . $increment;
195
			$increment++;
196
		}
197
198
		return $newName;
199
	}
200
201
	/**
202
	 * Check that moving the calendar won't break shares
203
	 *
204
	 * @param array $calendar
205
	 * @param string $userOrigin
206
	 * @param string $userDestination
207
	 * @param bool $force
208
	 * @return bool had any shares or not
209
	 * @throws \InvalidArgumentException
210
	 */
211
	private function checkShares(array $calendar, string $userOrigin, string $userDestination, bool $force = false): bool {
0 ignored issues
show
Unused Code introduced by
The parameter $userOrigin is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

211
	private function checkShares(array $calendar, /** @scrutinizer ignore-unused */ string $userOrigin, string $userDestination, bool $force = false): bool {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
212
		$shares = $this->calDav->getShares($calendar['id']);
213
		foreach ($shares as $share) {
214
			list(, $prefix, $userOrGroup) = explode('/', $share['href'], 3);
215
216
			/**
217
			 * Check that user destination is member of the groups which whom the calendar was shared
218
			 * If we ask to force the migration, the share with the group is dropped
219
			 */
220
			if ($this->shareManager->shareWithGroupMembersOnly() === true && 'groups' === $prefix && !$this->groupManager->isInGroup($userDestination, $userOrGroup)) {
221
				if ($force) {
222
					$this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config), [], ['href' => 'principal:principals/groups/' . $userOrGroup]);
223
				} else {
224
					throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . "> was shared. You may use -f to move the calendar while deleting this share.");
225
				}
226
			}
227
228
			/**
229
			 * Check that calendar isn't already shared with user destination
230
			 */
231
			if ($userOrGroup === $userDestination) {
232
				if ($force) {
233
					$this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config), [], ['href' => 'principal:principals/users/' . $userOrGroup]);
234
				} else {
235
					throw new \InvalidArgumentException("The calendar <" . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share.");
236
				}
237
			}
238
		}
239
240
		return count($shares) > 0;
241
	}
242
}
243