Completed
Pull Request — master (#32545)
by Tom
15:20 queued 06:23
created

TransferRequestManager   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 283
Duplicated Lines 7.42 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
dl 21
loc 283
rs 9.84
c 0
b 0
f 0
wmc 32
lcom 1
cbo 21

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
B newTransferRequest() 0 55 8
A acceptRequest() 3 20 4
A rejectRequest() 3 18 4
A deleteRequest() 0 13 1
A sendRequestNotification() 0 36 1
A prepare() 0 23 4
A formatNotification() 15 27 4
A cleanup() 0 24 3
A getLockTokenFromRequest() 0 3 1
A getRequestById() 0 3 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
namespace OCA\Files\Service\TransferOwnership;
4
5
use OC\Command\CommandJob;
6
use OCP\AppFramework\Db\DoesNotExistException;
7
use OCP\AppFramework\Utility\ITimeFactory;
8
use OCP\BackgroundJob\IJobList;
9
use OCP\Files\Folder;
10
use OCP\Files\IRootFolder;
11
use OCP\Files\NotFoundException;
12
use OCP\Files\NotPermittedException;
13
use OCP\Files\Storage\IPersistentLockingStorage;
14
use OCP\IL10N;
15
use OCP\IURLGenerator;
16
use OCP\IUser;
17
use OCP\IUserManager;
18
use OCP\L10N\IFactory;
19
use OCP\Lock\Persistent\ILock;
20
use OCP\Notification\IManager;
21
use OCP\Notification\INotification;
22
use OCP\Notification\INotifier;
23
use OCP\Util;
24
25
class TransferRequestManager implements INotifier {
26
27
	/** @var IRootFolder */
28
	protected $rootFolder;
29
	/** @var IManager */
30
	protected $notificationManager;
31
	/** @var IUserManager  */
32
	protected $userManager;
33
	/** @var ITimeFactory */
34
	protected $timeFactory;
35
	/** @var TransferRequestMapper  */
36
	protected $requestMapper;
37
	/** @var IURLGenerator  */
38
	protected $urlGenerator;
39
	/** @var IFactory */
40
	protected $factory;
41
	/** @var IJobList */
42
	protected $jobList;
43
44
	public function __construct(
45
		IRootFolder $rootFolder,
46
		IManager $notificationManager,
47
		IUserManager $userManager,
48
		ITimeFactory $timeFactory,
49
		TransferRequestMapper $requestMapper,
50
		IURLGenerator $urlGenerator,
51
		IFactory $factory,
52
		IJobList $jobList) {
53
		$this->rootFolder = $rootFolder;
54
		$this->notificationManager = $notificationManager;
55
		$this->userManager = $userManager;
56
		$this->timeFactory = $timeFactory;
57
		$this->requestMapper = $requestMapper;
58
		$this->urlGenerator = $urlGenerator;
59
		$this->factory = $factory;
60
		$this->jobList = $jobList;
61
	}
62
63
	/**
64
	 * @param IUser $sourceUser
65
	 * @param IUser $destinationUser
66
	 * @param $fileId
67
	 * @throws NotFoundException
68
	 * @throws \Exception
69
	 */
70
	public function newTransferRequest(IUser $sourceUser, IUser $destinationUser, $fileId) {
71
		// Cannot give to self
72
		if ($sourceUser->getUID() === $destinationUser->getUID()) {
73
			throw new \Exception('Cannot transfer to self');
74
		}
75
		// Check node exists
76
		$sourceFolder = $this->rootFolder->getById($fileId)[0];
77
		// Check source user owns the node
78
		if ($sourceFolder->getOwner()->getUID() !== $sourceUser->getUID()) {
79
			throw new NotPermittedException('Cannot move a file you dont own');
80
		}
81
		// Check the folder is on persistent lockable storage otherwise we can't do this in the background
82
		if (!$sourceFolder->getStorage() instanceof IPersistentLockingStorage) {
83
			throw new \Exception('Source folder storage not lockable');
84
		}
85
		// Check therer is no request with the same signature
86
		if (count($this->requestMapper->findRequestWithSameSignature($sourceUser->getUID(), $destinationUser->getUID(), $fileId)) > 0) {
87
			// There is
88
			throw new \Exception('There is already a request to transfer this file/folder');
89
		}
90
		// Check we are not trying to request a transfer for a folder that is inside a current request
91
		$folder = $sourceFolder;
92
		$fileids = [$folder->getId()];
93
		while($folder->getPath() !== '/') {
94
			$folder = $folder->getParent();
95
			$fileids[] = $folder->getId();
96
		}
97
		if (count($this->requestMapper->findOpenRequestForGivenFiles($fileids)) > 0) {
98
			throw new \Exception('This file/folder is already pending an existing transfer');
99
		}
100
101
		// Create the transfer request object
102
		$request = new TransferRequest();
103
		$request->setRequestedTime($this->timeFactory->getTime());
104
		$request->setSourceUserId($sourceUser->getUID());
105
		$request->setDestinationUserId($destinationUser->getUID());
106
		$request->setFileId($fileId);
107
		$request = $this->requestMapper->insert($request);
108
109
		/** @var IPersistentLockingStorage $storage */
110
		$storage = $sourceFolder->getStorage();
111
		try {
112
			$storage->lockNodePersistent($sourceFolder->getInternalPath(), [
113
				'depth' => ILock::LOCK_DEPTH_INFINITE,
114
				'token' => $this->getLockTokenFromRequest($request),
0 ignored issues
show
Compatibility introduced by
$request of type object<OCP\AppFramework\Db\Entity> is not a sub-type of object<OCA\Files\Service...ership\TransferRequest>. It seems like you assume a child class of the class OCP\AppFramework\Db\Entity to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
115
				'timeout' => 60*60*2 // 2 hours to allow a cron run
116
			]);
117
		} catch (\Exception $e) {
118
			// Cleanup transfer request and fail
119
			$this->requestMapper->delete($request);
120
			throw $e;
121
		}
122
123
		$this->sendRequestNotification($request);
0 ignored issues
show
Compatibility introduced by
$request of type object<OCP\AppFramework\Db\Entity> is not a sub-type of object<OCA\Files\Service...ership\TransferRequest>. It seems like you assume a child class of the class OCP\AppFramework\Db\Entity to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
124
	}
125
126
	public function acceptRequest(TransferRequest $request) {
127 View Code Duplication
		if ($request->getAcceptedTime() !== null || $request->getActionedTime() !== null || $request->getRejectedTime() !== null) {
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...
128
			throw new \Exception('Already actioned, accepted or rejected');
129
		}
130
		// Create a background job, update accepted time
131
		$request->setAcceptedTime($this->timeFactory->getTime());
132
		$this->requestMapper->update($request);
133
		$sourcePath = $this->rootFolder->getUserFolder(
134
			$request->getSourceUserId())->getById($request->getFileId())[0]->getInternalPath();
135
		$this->jobList->add(CommandJob::class, [
136
			'sourcePath' => $sourcePath,
137
			'sourceUser' => $request->getSourceUserId(),
138
			'destinationUser' => $request->getDestinationUserId()
139
		]);
140
		$notification = $this->notificationManager->createNotification();
141
		$notification->setApp('files')
142
			->setUser($request->getDestinationUserId())
143
			->setObject('transfer_request', $request->getId());
144
		$this->notificationManager->markProcessed($notification);
145
	}
146
147
	public function rejectRequest(TransferRequest $request) {
148
		// Cleanup the lock, save reject timestamp
149 View Code Duplication
		if ($request->getAcceptedTime() !== null || $request->getActionedTime() !== null || $request->getRejectedTime() !== null) {
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...
150
			throw new \Exception('Already actioned, accepted or rejected');
151
		}
152
		// Create a background job, update accepted time
153
		$request->setRejectedTime($this->timeFactory->getTime());
154
		$this->requestMapper->update($request);
155
		$notification = $this->notificationManager->createNotification();
156
		$notification->setApp('files')
157
			->setUser($request->getDestinationUserId())
158
			->setObject('transfer_request', $request->getId());
159
		$this->notificationManager->markProcessed($notification);
160
		$file = $this->rootFolder->getById($request->getFileId())[0];
161
		/** @var IPersistentLockingStorage $storage */
162
		$storage = $file->getStorage();
163
		$storage->unlockNodePersistent($file->getInternalPath(), ['token' => $this->getLockTokenFromRequest($request)]);
164
	}
165
166
	public function deleteRequest(TransferRequest $request) {
167
		// Cleanup the lock and the notification
168
		$this->requestMapper->delete($request);
169
		$notification = $this->notificationManager->createNotification();
170
		$notification->setApp('files')
171
			->setUser($request->getDestinationUserId())
172
			->setObject('transfer_request', $request->getId());
173
		$this->notificationManager->markProcessed($notification);
174
		$file = $this->rootFolder->getById($request->getFileId())[0];
175
		/** @var IPersistentLockingStorage $storage */
176
		$storage = $file->getStorage();
177
		$storage->unlockNodePersistent($file->getInternalPath(), ['token' => $this->getLockTokenFromRequest($request)]);
178
	}
179
180
181
	/**
182
	 * @param TransferRequest $request the request object
183
	 */
184
	protected function sendRequestNotification(TransferRequest $request) {
185
		$time = new \DateTime();
186
		$time->setTimestamp($this->timeFactory->getTime());
187
		$notification = $this->notificationManager->createNotification();
188
		$notification->setApp('files')
189
			->setUser($request->getDestinationUserId())
190
			->setDateTime($time)
191
			->setObject('transfer_request', $request->getId());
192
193
		$notification->setIcon(
194
			$this->urlGenerator->imagePath('core', 'actions/give.svg')
195
		);
196
		$notification->setLink('test.com');
197
198
		$sourceUser = $this->userManager->get($request->getSourceUserId());
199
		$folder = $this->rootFolder->getById($request->getFileId())[0];
200
		$notification->setSubject("new_transfer_request");
201
		$notification->setMessage("new_transfer_request", [$sourceUser->getDisplayName(), $folder->getName(), Util::humanFileSize($folder->getSize())]);
202
203
		$endpoint = $this->urlGenerator->linkToRouteAbsolute(
204
			'files.Transfer.accept',
205
			['requestId' => $request->getId()]
206
		);
207
		$declineAction = $notification->createAction();
208
		$declineAction->setLabel('reject');
209
		$declineAction->setLink($endpoint, 'DELETE');
210
		$notification->addAction($declineAction);
211
212
		$acceptAction = $notification->createAction();
213
		$acceptAction->setLabel('accept');
214
		$acceptAction->setLink($endpoint, 'POST');
215
		$acceptAction->setPrimary(true);
216
		$notification->addAction($acceptAction);
217
218
		$this->notificationManager->notify($notification);
219
	}
220
221
	public function prepare(INotification $notification, $languageCode) {
222
		if ($notification->getApp() !== 'files') {
223
			throw new \InvalidArgumentException();
224
		}
225
226
		// Read the language from the notification
227
		$l = $this->factory->get('files', $languageCode);
228
229
		switch ($notification->getObjectType()) {
230
			case 'transfer_request':
231
				$requestId = $notification->getObjectId();
232
				try {
233
					$this->requestMapper->findById($requestId);
234
				} catch (DoesNotExistException $ex) {
235
					$this->notificationManager->markProcessed($notification);
236
					throw new \InvalidArgumentException();
237
				}
238
				return $this->formatNotification($notification, $l);
239
240
			default:
241
				throw new \InvalidArgumentException();
242
		}
243
	}
244
245
	protected function formatNotification(INotification $notification, IL10N $l) {
246
		$notification->setParsedSubject((string) $l->t('A user would like to transfer a folder to you'));
247
248
		$notification->setParsedMessage(
249
			(string) $l->t(
250
				'"%1$s" requested to transfer "%2$s" to you (%3$s)"',
251
				$notification->getMessageParameters())
252
		);
253
254 View Code Duplication
		foreach ($notification->getActions() as $action) {
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...
255
			switch ($action->getLabel()) {
256
				case 'accept':
257
					$action->setParsedLabel(
258
						(string) $l->t('Accept')
259
					);
260
					break;
261
				case 'reject':
262
					$action->setParsedLabel(
263
						(string) $l->t('Decline')
264
					);
265
					break;
266
			}
267
			$notification->addParsedAction($action);
268
		}
269
270
		return $notification;
271
	}
272
273
	public function cleanup() {
274
		// Delete request that are older than 24 hours
275
		$oldRequests = $this->requestMapper->findOpenRequestsOlderThan(1);
276
		/** @var TransferRequest $request */
277
		foreach ($oldRequests as $request) {
278
			// Remove the lock
279
			try {
280
				$file = $this->rootFolder->getById($request->getFileId())[0];
281
				/** @var IPersistentLockingStorage $storage */
282
				$storage = $file->getStorage();
283
				$storage->unlockNodePersistent($file->getInternalPath(), $this->getLockTokenFromRequest($request));
0 ignored issues
show
Documentation introduced by
$this->getLockTokenFromRequest($request) is of type string, but the function expects a array.

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...
284
			} catch (\Exception $e) {
285
				\OC::$server->getLogger()->logException($e, ['app' => 'files']);
286
			}
287
			// Now remove the request
288
			$this->requestMapper->delete($request);
289
			// And lets remove the notification to save confusion
290
			$notification = $this->notificationManager->createNotification();
291
			$notification->setApp('files')
292
				->setUser($request->getDestinationUserId())
293
				->setObject('transfer_request', $request->getId());
294
			$this->notificationManager->markProcessed($notification);
295
		}
296
	}
297
298
	protected function getLockTokenFromRequest(TransferRequest $request) {
299
		return 'transfer-request-'.$request->getId();
300
	}
301
302
	public function getRequestById($requestId) {
303
		return $this->requestMapper->findById($requestId);
304
	}
305
306
307
}