WopiController::checkFileInfo()   F
last analyzed

Complexity

Conditions 19
Paths 10264

Size

Total Lines 99

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 380

Importance

Changes 0
Metric Value
dl 0
loc 99
ccs 0
cts 84
cp 0
rs 0.3018
c 0
b 0
f 0
cc 19
nc 10264
nop 2
crap 380

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016-2017 Lukas Reschke <[email protected]>
4
 *
5
 * @license GNU AGPL version 3 or any later version
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as
9
 * published by the Free Software Foundation, either version 3 of the
10
 * License, or (at your option) any later version.
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
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 *
20
 */
21
22
namespace OCA\Richdocuments\Controller;
23
24
use OC\Files\View;
25
use OCA\Richdocuments\Db\Wopi;
26
use OCA\Richdocuments\AppConfig;
27
use OCA\Richdocuments\Db\WopiMapper;
28
use OCA\Richdocuments\Service\UserScopeService;
29
use OCA\Richdocuments\TemplateManager;
30
use OCA\Richdocuments\TokenManager;
31
use OCA\Richdocuments\Helper;
32
use OCP\AppFramework\Controller;
33
use OCP\AppFramework\Db\DoesNotExistException;
34
use OCP\AppFramework\Http;
35
use OCP\AppFramework\Http\JSONResponse;
36
use OCP\Files\File;
37
use OCP\Files\Folder;
38
use OCP\Files\GenericFileException;
39
use OCP\Files\InvalidPathException;
40
use OCP\Files\IRootFolder;
41
use OCP\Files\Node;
42
use OCP\Files\NotFoundException;
43
use OCP\Files\NotPermittedException;
44
use OCP\IConfig;
45
use OCP\ILogger;
46
use OCP\IRequest;
47
use OCP\IURLGenerator;
48
use OCP\AppFramework\Http\StreamResponse;
49
use OCP\IUserManager;
50
use OCP\Lock\LockedException;
51
use OCP\Share\Exceptions\ShareNotFound;
52
use OCP\Share\IManager;
53
54
class WopiController extends Controller {
55
	/** @var IRootFolder */
56
	private $rootFolder;
57
	/** @var IURLGenerator */
58
	private $urlGenerator;
59
	/** @var IConfig */
60
	private $config;
61
	/** @var AppConfig */
62
	private $appConfig;
63
	/** @var TokenManager */
64
	private $tokenManager;
65
	/** @var IUserManager */
66
	private $userManager;
67
	/** @var WopiMapper */
68
	private $wopiMapper;
69
	/** @var ILogger */
70
	private $logger;
71
	/** @var TemplateManager */
72
	private $templateManager;
73
	/** @var IManager */
74
	private $shareManager;
75
	/** @var UserScopeService */
76
	private $userScopeService;
77
78
	// Signifies LOOL that document has been changed externally in this storage
79
	const LOOL_STATUS_DOC_CHANGED = 1010;
80
81
	/**
82
	 * @param string $appName
83
	 * @param IRequest $request
84
	 * @param IRootFolder $rootFolder
85
	 * @param IURLGenerator $urlGenerator
86
	 * @param IConfig $config
87
	 * @param TokenManager $tokenManager
88
	 * @param IUserManager $userManager
89
	 * @param WopiMapper $wopiMapper
90
	 * @param ILogger $logger
91
	 * @param TemplateManager $templateManager
92
	 */
93 View Code Duplication
	public function __construct(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
94
		$appName,
95
		IRequest $request,
96
		IRootFolder $rootFolder,
97
		IURLGenerator $urlGenerator,
98
		IConfig $config,
99
		AppConfig $appConfig,
100
		TokenManager $tokenManager,
101
		IUserManager $userManager,
102
		WopiMapper $wopiMapper,
103
		ILogger $logger,
104
		TemplateManager $templateManager,
105
		IManager $shareManager,
106
		UserScopeService $userScopeService
107
	) {
108
		parent::__construct($appName, $request);
109
		$this->rootFolder = $rootFolder;
110
		$this->urlGenerator = $urlGenerator;
111
		$this->config = $config;
112
		$this->appConfig = $appConfig;
113
		$this->tokenManager = $tokenManager;
114
		$this->userManager = $userManager;
115
		$this->wopiMapper = $wopiMapper;
116
		$this->logger = $logger;
117
		$this->templateManager = $templateManager;
118
		$this->shareManager = $shareManager;
119
		$this->userScopeService = $userScopeService;
120
	}
121
122
	/**
123
	 * Returns general info about a file.
124
	 *
125
	 * @NoAdminRequired
126
	 * @NoCSRFRequired
127
	 * @PublicPage
128
	 *
129
	 * @param string $fileId
130
	 * @param string $access_token
131
	 * @return JSONResponse
132
	 * @throws InvalidPathException
133
	 * @throws NotFoundException
134
	 */
135
	public function checkFileInfo($fileId, $access_token) {
136
		try {
137
			list($fileId, , $version) = Helper::parseFileId($fileId);
138
139
			$wopi = $this->wopiMapper->getWopiForToken($access_token);
140
			if ($wopi->isTemplateToken()) {
141
				$this->templateManager->setUserId($wopi->getOwnerUid());
142
				$file = $this->templateManager->get($wopi->getFileid());
143
			} else {
144
				$file = $this->getFileForWopiToken($wopi);
145
			}
146
			if(!($file instanceof File)) {
147
				throw new NotFoundException('No valid file found for ' . $fileId);
148
			}
149
		} catch (NotFoundException $e) {
150
			$this->logger->debug($e->getMessage(), ['app' => 'richdocuments', '']);
0 ignored issues
show
Deprecated Code introduced by
The method OCP\ILogger::debug() has been deprecated with message: 20.0.0 use \Psr\Log\LoggerInterface::debug

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
151
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
152
		} catch (DoesNotExistException $e) {
153
			$this->logger->debug($e->getMessage(), ['app' => 'richdocuments', '']);
0 ignored issues
show
Deprecated Code introduced by
The method OCP\ILogger::debug() has been deprecated with message: 20.0.0 use \Psr\Log\LoggerInterface::debug

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
154
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
155
		} catch (\Exception $e) {
156
			$this->logger->logException($e, ['app' => 'richdocuments']);
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Throwable>.

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...
Deprecated Code introduced by
The method OCP\ILogger::logException() has been deprecated with message: 20.0.0 use the `exception` entry in the context of any method in \Psr\Log\LoggerInterface

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
157
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
158
		}
159
160
		$isPublic = empty($wopi->getEditorUid());
161
		$guestUserId = 'Guest-' . \OC::$server->getSecureRandom()->generate(8);
162
		$user = $this->userManager->get($wopi->getEditorUid());
163
		$userDisplayName = $user !== null && !$isPublic ? $user->getDisplayName() : $wopi->getGuestDisplayname();
164
		$isVersion = $version !== '0';
165
		$response = [
166
			'BaseFileName' => $file->getName(),
167
			'Size' => $file->getSize(),
168
			'Version' => $version,
169
			'UserId' => !$isPublic ? $wopi->getEditorUid() : $guestUserId,
170
			'OwnerId' => $wopi->getOwnerUid(),
171
			'UserFriendlyName' => $userDisplayName,
172
			'UserExtraInfo' => [
173
			],
174
			'UserCanWrite' => (bool)$wopi->getCanwrite(),
175
			'UserCanNotWriteRelative' => \OC::$server->getEncryptionManager()->isEnabled() || $isPublic,
176
			'PostMessageOrigin' => $wopi->getServerHost(),
177
			'LastModifiedTime' => Helper::toISO8601($file->getMTime()),
178
			'SupportsRename' => !$isVersion,
179
			'UserCanRename' => !$isPublic && !$isVersion,
180
			'EnableInsertRemoteImage' => true,
181
			'EnableShare' => $file->isShareable() && !$isVersion,
182
			'HideUserList' => 'desktop',
183
			'DisablePrint' => $wopi->getHideDownload(),
184
			'DisableExport' => $wopi->getHideDownload(),
185
			'DisableCopy' => $wopi->getHideDownload(),
186
			'HideExportOption' => $wopi->getHideDownload(),
187
			'HidePrintOption' => $wopi->getHideDownload(),
188
			'DownloadAsPostMessage' => $wopi->getDirect(),
189
		];
190
191
		if ($wopi->isTemplateToken()) {
192
			$userFolder = $this->rootFolder->getUserFolder($wopi->getOwnerUid());
193
			$file = $userFolder->getById($wopi->getTemplateDestination())[0];
194
			$response['TemplateSaveAs'] = $file->getName();
195
		}
196
197
		if ($this->shouldWatermark($isPublic, $wopi->getEditorUid(), $fileId, $wopi)) {
198
			$email = $user !== null && !$isPublic ? $user->getEMailAddress() : "";
199
			$replacements = [
200
				'userId' => $wopi->getEditorUid(),
201
				'date' => (new \DateTime())->format('Y-m-d H:i:s'),
202
				'themingName' => \OC::$server->getThemingDefaults()->getName(),
203
				'userDisplayName' => $userDisplayName,
204
				'email' => $email,
205
206
			];
207
			$watermarkTemplate = $this->appConfig->getAppValue('watermark_text');
208
			$response['WatermarkText'] = preg_replace_callback('/{(.+?)}/', function ($matches) use ($replacements) {
209
				return $replacements[$matches[1]];
210
			}, $watermarkTemplate);
211
		}
212
213
		/**
214
		 * New approach for generating files from templates by creating an empty file
215
		 * and providing an URL which returns the actual template
216
		 */
217
		if ($wopi->hasTemplateId()) {
218
			$templateUrl = 'index.php/apps/richdocuments/wopi/template/' . $wopi->getTemplateId() . '?access_token=' . $wopi->getToken();
219
			$templateUrl = $this->urlGenerator->getAbsoluteURL($templateUrl);
220
			$response['TemplateSource'] = $templateUrl;
221
		}
222
223
		$user = $this->userManager->get($wopi->getEditorUid());
224
		if($user !== null) {
225
			$response['UserExtraInfo']['avatar'] = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $wopi->getEditorUid(), 'size' => 32]);
226
		}
227
228
		if (!empty($wopi->getRemoteServer())) {
0 ignored issues
show
Documentation Bug introduced by
The method getRemoteServer does not exist on object<OCA\Richdocuments\Db\Wopi>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
229
			$response = $this->setFederationFileInfo($wopi, $response);
230
		}
231
232
		return new JSONResponse($response);
233
	}
234
235
	private function setFederationFileInfo($wopi, $response) {
236
		$remoteUserId = $wopi->getGuestDisplayname();
237
		$cloudID = \OC::$server->getCloudIdManager()->resolveCloudId($remoteUserId);
238
		$response['UserFriendlyName'] = $cloudID->getDisplayId();
239
		$response['UserExtraInfo']['avatar'] = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => explode('@', $remoteUserId)[0], 'size' => 32]);
240
		$cleanCloudId = str_replace(['http://', 'https://'], '', $cloudID->getId());
241
		$addressBookEntries = \OC::$server->getContactsManager()->search($cleanCloudId, ['CLOUD']);
242
		foreach ($addressBookEntries as $entry) {
243
			if (isset($entry['CLOUD'])) {
244
				foreach ($entry['CLOUD'] as $cloudID) {
245
					if ($cloudID === $cleanCloudId) {
246
						$response['UserFriendlyName'] = $entry['FN'];
247
						break;
248
					}
249
				}
250
			}
251
		}
252
		return $response;
253
	}
254
255
	private function shouldWatermark($isPublic, $userId, $fileId, Wopi $wopi) {
256
		if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_enabled', 'no') === 'no') {
257
			return false;
258
		}
259
260
		if ($isPublic) {
261
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkAll', 'no') === 'yes') {
262
				return true;
263
			}
264 View Code Duplication
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkRead', 'no') === 'yes' && !$wopi->getCanwrite()) {
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...
265
				return true;
266
			}
267
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkSecure', 'no') === 'yes' && $wopi->getHideDownload()) {
268
				return true;
269
			}
270 View Code Duplication
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkTags', 'no') === 'yes') {
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...
271
				$tags = $this->appConfig->getAppValueArray('watermark_linkTagsList');
272
				$fileTags = \OC::$server->getSystemTagObjectMapper()->getTagIdsForObjects([$fileId], 'files')[$fileId];
273
				foreach ($fileTags as $tagId) {
274
					if (in_array($tagId, $tags, true)) {
275
						return true;
276
					}
277
				}
278
			}
279
		} else {
280
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareAll', 'no') === 'yes') {
281
				$files = $this->rootFolder->getUserFolder($userId)->getById($fileId);
282
				if (count($files) !== 0 && $files[0]->getOwner()->getUID() !== $userId) {
283
					return true;
284
				}
285
			}
286 View Code Duplication
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareRead', 'no') === 'yes' && !$wopi->getCanwrite()) {
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...
287
				return true;
288
			}
289
		}
290
		if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allGroups', 'no') === 'yes') {
291
			$groups = $this->appConfig->getAppValueArray('watermark_allGroupsList');
292
			foreach ($groups as $group) {
0 ignored issues
show
Bug introduced by
The expression $groups of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
293
				if (\OC::$server->getGroupManager()->isInGroup($userId, $group)) {
294
					return true;
295
				}
296
			}
297
		}
298 View Code Duplication
		if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allTags', 'no') === 'yes') {
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...
299
			$tags = $this->appConfig->getAppValueArray('watermark_allTagsList');
300
			$fileTags = \OC::$server->getSystemTagObjectMapper()->getTagIdsForObjects([$fileId], 'files')[$fileId];
301
			foreach ($fileTags as $tagId) {
302
				if (in_array($tagId, $tags, true)) {
303
					return true;
304
				}
305
			}
306
		}
307
308
		return false;
309
	}
310
311
	/**
312
	 * Given an access token and a fileId, returns the contents of the file.
313
	 * Expects a valid token in access_token parameter.
314
	 *
315
	 * @PublicPage
316
	 * @NoCSRFRequired
317
	 *
318
	 * @param string $fileId
319
	 * @param string $access_token
320
	 * @return Http\Response
321
	 * @throws DoesNotExistException
322
	 * @throws NotFoundException
323
	 * @throws NotPermittedException
324
	 */
325
	public function getFile($fileId,
326
							$access_token) {
327
		list($fileId, , $version) = Helper::parseFileId($fileId);
328
329
		$wopi = $this->wopiMapper->getWopiForToken($access_token);
330
331
		if ((int)$fileId !== $wopi->getFileid()) {
332
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
333
		}
334
335
		// Template is just returned as there is no version logic
336
		if ($wopi->isTemplateToken()) {
337
			$this->templateManager->setUserId($wopi->getOwnerUid());
338
			$file = $this->templateManager->get($wopi->getFileid());
339
			$response = new StreamResponse($file->fopen('rb'));
340
			$response->addHeader('Content-Disposition', 'attachment');
341
			$response->addHeader('Content-Type', 'application/octet-stream');
342
			return $response;
343
		}
344
345
		try {
346
			/** @var File $file */
347
			$userFolder = $this->rootFolder->getUserFolder($wopi->getOwnerUid());
348
			$file = $userFolder->getById($fileId)[0];
349
			\OC_User::setIncognitoMode(true);
350
			if ($version !== '0') {
351
				$view = new View('/' . $wopi->getOwnerUid() . '/files');
352
				$relPath = $view->getRelativePath($file->getPath());
353
				$versionPath = '/files_versions/' . $relPath . '.v' . $version;
354
				$view = new View('/' . $wopi->getOwnerUid());
355
				if ($view->file_exists($versionPath)){
356
					$info = $view->getFileInfo($versionPath);
357 View Code Duplication
					if ($info->getSize() === 0) {
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...
358
						$response = new Http\Response();
359
					} else {
360
						$response = new StreamResponse($view->fopen($versionPath, 'rb'));
361
					}
362
				}
363
				else {
364
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
365
				}
366
			}
367 View Code Duplication
			else {
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...
368
				if ($file->getSize() === 0) {
369
					$response = new Http\Response();
370
				} else {
371
					$response = new StreamResponse($file->fopen('rb'));
0 ignored issues
show
Bug introduced by
The method fopen() does not seem to exist on object<OCP\Files\Node>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
372
				}
373
			}
374
			$response->addHeader('Content-Disposition', 'attachment');
375
			$response->addHeader('Content-Type', 'application/octet-stream');
376
			return $response;
377
		} catch (\Exception $e) {
378
			$this->logger->logException($e, ['level' => ILogger::ERROR,	'app' => 'richdocuments', 'message' => 'getFile failed']);
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Throwable>.

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...
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated with message: 20.0.0

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The method OCP\ILogger::logException() has been deprecated with message: 20.0.0 use the `exception` entry in the context of any method in \Psr\Log\LoggerInterface

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
379
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
380
		}
381
	}
382
383
	/**
384
	 * Given an access token and a fileId, replaces the files with the request body.
385
	 * Expects a valid token in access_token parameter.
386
	 *
387
	 * @PublicPage
388
	 * @NoCSRFRequired
389
	 *
390
	 * @param string $fileId
391
	 * @param string $access_token
392
	 * @return JSONResponse
393
	 * @throws DoesNotExistException
394
	 */
395
	public function putFile($fileId,
396
							$access_token) {
397
		list($fileId, ,) = Helper::parseFileId($fileId);
398
		$isPutRelative = ($this->request->getHeader('X-WOPI-Override') === 'PUT_RELATIVE');
399
		$isRenameFile = ($this->request->getHeader('X-WOPI-Override') === 'RENAME_FILE');
0 ignored issues
show
Unused Code introduced by
$isRenameFile is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
400
401
		$wopi = $this->wopiMapper->getWopiForToken($access_token);
402
		if (!$wopi->getCanwrite()) {
403
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
404
		}
405
406
		// Set the user to register the change under his name
407
		$this->userScopeService->setUserScope($wopi->getUserForFileAccess());
408
		$this->userScopeService->setFilesystemScope($isPutRelative ? $wopi->getEditorUid() : $wopi->getUserForFileAccess());
409
410
		try {
411
			if ($isPutRelative) {
412
				// the new file needs to be installed in the current user dir
413
				$userFolder = $this->rootFolder->getUserFolder($wopi->getEditorUid());
414
				$file = $userFolder->getById($fileId);
415
				if (count($file) === 0) {
416
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
417
				}
418
				$file = $file[0];
419
				$suggested = $this->request->getHeader('X-WOPI-SuggestedTarget');
420
				$suggested = iconv('utf-7', 'utf-8', $suggested);
421
422 View Code Duplication
				if ($suggested[0] === '.') {
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...
423
					$path = dirname($file->getPath()) . '/New File' . $suggested;
424
				}
425
				else if ($suggested[0] !== '/') {
426
					$path = dirname($file->getPath()) . '/' . $suggested;
427
				}
428
				else {
429
					$path = $userFolder->getPath() . $suggested;
430
				}
431
432
				if ($path === '') {
433
					return new JSONResponse([
434
						'status' => 'error',
435
						'message' => 'Cannot create the file'
436
					]);
437
				}
438
439
				// create the folder first
440
				if (!$this->rootFolder->nodeExists(dirname($path))) {
441
					$this->rootFolder->newFolder(dirname($path));
442
				}
443
444
				// create a unique new file
445
				$path = $this->rootFolder->getNonExistingName($path);
446
				$this->rootFolder->newFile($path);
447
				$file = $this->rootFolder->get($path);
448
			} else {
449
				$file = $this->getFileForWopiToken($wopi);
450
				$wopiHeaderTime = $this->request->getHeader('X-LOOL-WOPI-Timestamp');
451
452
				if ($wopiHeaderTime !== null && $wopiHeaderTime !== Helper::toISO8601($file->getMTime() ?? 0)) {
453
					$this->logger->debug('Document timestamp mismatch ! WOPI client says mtime {headerTime} but storage says {storageTime}', [
0 ignored issues
show
Deprecated Code introduced by
The method OCP\ILogger::debug() has been deprecated with message: 20.0.0 use \Psr\Log\LoggerInterface::debug

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
454
						'headerTime' => $wopiHeaderTime,
455
						'storageTime' => Helper::toISO8601($file->getMTime() ?? 0)
456
					]);
457
					// Tell WOPI client about this conflict.
458
					return new JSONResponse(['LOOLStatusCode' => self::LOOL_STATUS_DOC_CHANGED], Http::STATUS_CONFLICT);
459
				}
460
			}
461
462
			$content = fopen('php://input', 'rb');
463
464
			try {
465
				$this->retryOperation(function () use ($file, $content){
466
					return $file->putContent($content);
467
				});
468
			} catch (LockedException $e) {
469
				$this->logger->logException($e);
0 ignored issues
show
Documentation introduced by
$e is of type object<OCP\Lock\LockedException>, but the function expects a object<Throwable>.

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...
Deprecated Code introduced by
The method OCP\ILogger::logException() has been deprecated with message: 20.0.0 use the `exception` entry in the context of any method in \Psr\Log\LoggerInterface

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
470
				return new JSONResponse(['message' => 'File locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
471
			}
472
473
			if ($isPutRelative) {
474
				// generate a token for the new file (the user still has to be
475
				// logged in)
476
				list(, $wopiToken) = $this->tokenManager->getToken($file->getId(), null, $wopi->getEditorUid());
477
478
				$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
479
				$url = $this->urlGenerator->getAbsoluteURL($wopi);
480
481
				return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
482
			}
483
			if ($wopi->hasTemplateId()) {
484
				$wopi->setTemplateId(null);
485
				$this->wopiMapper->update($wopi);
0 ignored issues
show
Deprecated Code introduced by
The method OCP\AppFramework\Db\Mapper::update() has been deprecated with message: 14.0.0 Move over to QBMapper

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
486
			}
487
			return new JSONResponse(['LastModifiedTime' => Helper::toISO8601($file->getMTime())]);
488
		} catch (\Exception $e) {
489
			$this->logger->logException($e, ['level' => ILogger::ERROR,	'app' => 'richdocuments', 'message' => 'getFile failed']);
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Throwable>.

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...
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated with message: 20.0.0

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The method OCP\ILogger::logException() has been deprecated with message: 20.0.0 use the `exception` entry in the context of any method in \Psr\Log\LoggerInterface

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
490
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
491
		}
492
	}
493
494
	/**
495
	 * Given an access token and a fileId, replaces the files with the request body.
496
	 * Expects a valid token in access_token parameter.
497
	 * Just actually routes to the PutFile, the implementation of PutFile
498
	 * handles both saving and saving as.* Given an access token and a fileId, replaces the files with the request body.
499
	 *
500
	 * FIXME Cleanup this code as is a lot of shared logic between putFile and putRelativeFile
501
	 *
502
	 * @PublicPage
503
	 * @NoCSRFRequired
504
	 *
505
	 * @param string $fileId
506
	 * @param string $access_token
507
	 * @return JSONResponse
508
	 * @throws DoesNotExistException
509
	 */
510
	public function putRelativeFile($fileId,
511
					$access_token) {
512
		list($fileId, ,) = Helper::parseFileId($fileId);
513
		$wopi = $this->wopiMapper->getWopiForToken($access_token);
514
		$isRenameFile = ($this->request->getHeader('X-WOPI-Override') === 'RENAME_FILE');
515
516
		if (!$wopi->getCanwrite()) {
517
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
518
		}
519
520
		// Unless the editor is empty (public link) we modify the files as the current editor
521
		$editor = $wopi->getEditorUid();
522
		if ($editor === null || !empty($wopi->getRemoteServer())) {
0 ignored issues
show
Documentation Bug introduced by
The method getRemoteServer does not exist on object<OCA\Richdocuments\Db\Wopi>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
523
			$editor = $wopi->getOwnerUid();
524
		}
525
526
		try {
527
			// the new file needs to be installed in the current user dir
528
			$userFolder = $this->rootFolder->getUserFolder($editor);
529
530
			if ($wopi->isTemplateToken()) {
531
				$this->templateManager->setUserId($wopi->getOwnerUid());
532
				$file = $userFolder->getById($wopi->getTemplateDestination())[0];
533
			} else if ($isRenameFile) {
534
				// the new file needs to be installed in the current user dir
535
				$userFolder = $this->rootFolder->getUserFolder($wopi->getEditorUid());
536
				$file = $userFolder->getById($fileId)[0];
537
538
				$suggested = $this->request->getHeader('X-WOPI-RequestedName');
539
540
				$suggested = iconv('utf-7', 'utf-8', $suggested) . '.' . $file->getExtension();
541
542
				if (strpos($suggested, '.') === 0) {
543
					$path = dirname($file->getPath()) . '/New File' . $suggested;
544
				}
545
				else if (strpos($suggested, '/') !== 0) {
546
					$path = dirname($file->getPath()) . '/' . $suggested;
547
				}
548
				else {
549
					$path = $userFolder->getPath() . $suggested;
550
				}
551
552
				if ($path === '') {
553
					return new JSONResponse([
554
						'status' => 'error',
555
						'message' => 'Cannot rename the file'
556
					]);
557
				}
558
559
				// create the folder first
560
				if (!$this->rootFolder->nodeExists(dirname($path))) {
561
					$this->rootFolder->newFolder(dirname($path));
562
				}
563
564
				// create a unique new file
565
				$path = $this->rootFolder->getNonExistingName($path);
566
				$file = $file->move($path);
567
			} else {
568
				$file = $userFolder->getById($fileId);
569
				if (count($file) === 0) {
570
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
571
				}
572
				$file = $file[0];
573
574
				$suggested = $this->request->getHeader('X-WOPI-SuggestedTarget');
575
				$suggested = iconv('utf-7', 'utf-8', $suggested);
576
577 View Code Duplication
				if ($suggested[0] === '.') {
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...
578
					$path = dirname($file->getPath()) . '/New File' . $suggested;
579
				} else if ($suggested[0] !== '/') {
580
					$path = dirname($file->getPath()) . '/' . $suggested;
581
				} else {
582
					$path = $userFolder->getPath() . $suggested;
583
				}
584
585
				if ($path === '') {
586
					return new JSONResponse([
587
						'status' => 'error',
588
						'message' => 'Cannot create the file'
589
					]);
590
				}
591
592
				// create the folder first
593
				if (!$this->rootFolder->nodeExists(dirname($path))) {
594
					$this->rootFolder->newFolder(dirname($path));
595
				}
596
597
				// create a unique new file
598
				$path = $this->rootFolder->getNonExistingName($path);
599
				$file = $this->rootFolder->newFile($path);
600
			}
601
602
			$content = fopen('php://input', 'rb');
603
			// Set the user to register the change under his name
604
			$this->userScopeService->setUserScope($wopi->getEditorUid());
605
			$this->userScopeService->setFilesystemScope($wopi->getEditorUid());
606
607
			try {
608
				$this->retryOperation(function () use ($file, $content){
609
					return $file->putContent($content);
0 ignored issues
show
Bug introduced by
The method putContent() does not seem to exist on object<OCP\Files\Node>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
610
				});
611
			} catch (LockedException $e) {
612
				return new JSONResponse(['message' => 'File locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
613
			}
614
615
			// generate a token for the new file (the user still has to be
616
			// logged in)
617
			list(, $wopiToken) = $this->tokenManager->getToken($file->getId(), null, $wopi->getEditorUid());
618
619
			$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
620
			$url = $this->urlGenerator->getAbsoluteURL($wopi);
621
622
			return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
623
		} catch (\Exception $e) {
624
			$this->logger->logException($e, ['level' => ILogger::ERROR,	'app' => 'richdocuments', 'message' => 'putRelativeFile failed']);
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Throwable>.

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...
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated with message: 20.0.0

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The method OCP\ILogger::logException() has been deprecated with message: 20.0.0 use the `exception` entry in the context of any method in \Psr\Log\LoggerInterface

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
625
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
626
		}
627
	}
628
629
	/**
630
	 * Retry operation if a LockedException occurred
631
	 * Other exceptions will still be thrown
632
	 * @param callable $operation
633
	 * @throws LockedException
634
	 * @throws GenericFileException
635
	 */
636
	private function retryOperation(callable $operation) {
637
		for ($i = 0; $i < 5; $i++) {
638
			try {
639
				if ($operation() !== false) {
640
					return;
641
				}
642
			} catch (LockedException $e) {
643
				if ($i === 4) {
644
					throw $e;
645
				}
646
				usleep(500000);
647
			}
648
		}
649
		throw new GenericFileException('Operation failed after multiple retries');
650
	}
651
652
	/**
653
	 * @param Wopi $wopi
654
	 * @return File|Folder|Node|null
655
	 * @throws NotFoundException
656
	 * @throws ShareNotFound
657
	 */
658
	private function getFileForWopiToken(Wopi $wopi) {
659
		$file = null;
0 ignored issues
show
Unused Code introduced by
$file is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
660
661
		if (!empty($wopi->getRemoteServer())) {
0 ignored issues
show
Documentation Bug introduced by
The method getRemoteServer does not exist on object<OCA\Richdocuments\Db\Wopi>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
662
			$share = $this->shareManager->getShareByToken($wopi->getEditorUid());
663
			$node = $share->getNode();
664
			if ($node instanceof Folder) {
665
				$file = $node->getById($wopi->getFileid())[0];
666
			} else {
667
				$file = $node;
668
			}
669
		} else {
670
			// Unless the editor is empty (public link) we modify the files as the current editor
671
			// TODO: add related share token to the wopi table so we can obtain the
672
			$userFolder = $this->rootFolder->getUserFolder($wopi->getUserForFileAccess());
673
			$files = $userFolder->getById($wopi->getFileid());
674
			if (isset($files[0]) && $files[0] instanceof File) {
675
				$file = $files[0];
676
			} else {
677
				throw new NotFoundException('No valid file found for wopi token');
678
			}
679
		}
680
		return $file;
681
	}
682
683
	/**
684
	 * Endpoint to return the template file that is requested by collabora to create a new document
685
	 *
686
	 * @PublicPage
687
	 * @NoCSRFRequired
688
	 *
689
	 * @param $fileId
690
	 * @param $access_token
691
	 * @return JSONResponse|StreamResponse
692
	 */
693
	public function getTemplate($fileId, $access_token) {
694
		try {
695
			$wopi = $this->wopiMapper->getPathForToken($access_token);
0 ignored issues
show
Deprecated Code introduced by
The method OCA\Richdocuments\Db\WopiMapper::getPathForToken() has been deprecated.

This method has been deprecated.

Loading history...
696
		} catch (DoesNotExistException $e) {
697
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
698
		}
699
700
		if ((int)$fileId !== $wopi->getTemplateId()) {
701
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
702
		}
703
704
		try {
705
			$this->templateManager->setUserId($wopi->getOwnerUid());
706
			$file = $this->templateManager->get($wopi->getTemplateId());
707
			$response = new StreamResponse($file->fopen('rb'));
708
			$response->addHeader('Content-Disposition', 'attachment');
709
			$response->addHeader('Content-Type', 'application/octet-stream');
710
			return $response;
711
		} catch (\Exception $e) {
712
			$this->logger->logException($e, ['level' => ILogger::ERROR,	'app' => 'richdocuments', 'message' => 'getTemplate failed']);
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Throwable>.

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...
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated with message: 20.0.0

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The method OCP\ILogger::logException() has been deprecated with message: 20.0.0 use the `exception` entry in the context of any method in \Psr\Log\LoggerInterface

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
713
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
714
		}
715
	}
716
717
}
718