Completed
Push — master ( 4f2c57...9702da )
by Julius
09:21 queued 06:37
created

WopiController::putFile()   D

Complexity

Conditions 14
Paths 148

Size

Total Lines 94

Duplication

Lines 9
Ratio 9.57 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 0
Metric Value
dl 9
loc 94
ccs 0
cts 73
cp 0
rs 4.8242
c 0
b 0
f 0
cc 14
nc 148
nop 2
crap 210

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
	public function __construct(
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', '']);
151
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
152
		} catch (DoesNotExistException $e) {
153
			$this->logger->debug($e->getMessage(), ['app' => 'richdocuments', '']);
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...
157
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
158
		}
159
160
		$isPublic = $wopi->getEditorUid() === null;
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
		$response = [
165
			'BaseFileName' => $file->getName(),
166
			'Size' => $file->getSize(),
167
			'Version' => $version,
168
			'UserId' => !$isPublic ? $wopi->getEditorUid() : $guestUserId,
169
			'OwnerId' => $wopi->getOwnerUid(),
170
			'UserFriendlyName' => $userDisplayName,
171
			'UserExtraInfo' => [
172
			],
173
			'UserCanWrite' => $wopi->getCanwrite(),
174
			'UserCanNotWriteRelative' => \OC::$server->getEncryptionManager()->isEnabled() || $isPublic,
175
			'PostMessageOrigin' => $wopi->getServerHost(),
176
			'LastModifiedTime' => Helper::toISO8601($file->getMTime()),
177
			'SupportsRename' => true,
178
			'UserCanRename' => !$isPublic,
179
			'EnableInsertRemoteImage' => true,
180
			'EnableShare' => true,
181
			'HideUserList' => 'desktop',
182
			'DisablePrint' => $wopi->getHideDownload(),
0 ignored issues
show
Documentation Bug introduced by
The method getHideDownload 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...
183
			'DisableExport' => $wopi->getHideDownload(),
0 ignored issues
show
Documentation Bug introduced by
The method getHideDownload 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...
184
			'DisableCopy' => $wopi->getHideDownload(),
0 ignored issues
show
Documentation Bug introduced by
The method getHideDownload 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...
185
			'HideExportOption' => $wopi->getHideDownload(),
0 ignored issues
show
Documentation Bug introduced by
The method getHideDownload 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...
186
			'HidePrintOption' => $wopi->getHideDownload(),
0 ignored issues
show
Documentation Bug introduced by
The method getHideDownload 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...
187
			'DownloadAsPostMessage' => $wopi->getDirect(),
0 ignored issues
show
Documentation Bug introduced by
The method getDirect 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...
188
		];
189
190
		if ($wopi->isTemplateToken()) {
191
			$userFolder = $this->rootFolder->getUserFolder($wopi->getOwnerUid());
192
			$file = $userFolder->getById($wopi->getTemplateDestination())[0];
193
			$response['TemplateSaveAs'] = $file->getName();
194
		}
195
196
		if ($this->shouldWatermark($isPublic, $wopi->getEditorUid(), $fileId, $wopi)) {
197
			$replacements = [
198
				'userId' => $wopi->getEditorUid(),
199
				'date' => (new \DateTime())->format('Y-m-d H:i:s'),
200
				'themingName' => \OC::$server->getThemingDefaults()->getName(),
201
202
			];
203
			$watermarkTemplate = $this->appConfig->getAppValue('watermark_text');
204
			$response['WatermarkText'] = preg_replace_callback('/{(.+?)}/', function ($matches) use ($replacements) {
205
				return $replacements[$matches[1]];
206
			}, $watermarkTemplate);
207
		}
208
209
		/**
210
		 * New approach for generating files from templates by creating an empty file
211
		 * and providing an URL which returns the actual templyte
212
		 */
213
		if ($wopi->hasTemplateId()) {
214
			$templateUrl = 'index.php/apps/richdocuments/wopi/template/' . $wopi->getTemplateId() . '?access_token=' . $wopi->getToken();
215
			$templateUrl = $this->urlGenerator->getAbsoluteURL($templateUrl);
216
			$response['TemplateSource'] = $templateUrl;
217
		}
218
219
		$user = $this->userManager->get($wopi->getEditorUid());
220
		if($user !== null && $user->getAvatarImage(32) !== null) {
221
			$response['UserExtraInfo']['avatar'] = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $wopi->getEditorUid(), 'size' => 32]);
222
		}
223
224
		if ($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...
225
			$response = $this->setFederationFileInfo($wopi, $response);
226
		}
227
228
		return new JSONResponse($response);
229
	}
230
231
	private function setFederationFileInfo($wopi, $response) {
232
		$remoteUserId = $wopi->getGuestDisplayname();
233
		$cloudID = \OC::$server->getCloudIdManager()->resolveCloudId($remoteUserId);
234
		$response['UserFriendlyName'] = $cloudID->getDisplayId();
235
		$response['UserExtraInfo']['avatar'] = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => explode('@', $remoteUserId)[0], 'size' => 32]);
236
		$cleanCloudId = str_replace(['http://', 'https://'], '', $cloudID->getId());
237
		$addressBookEntries = \OC::$server->getContactsManager()->search($cleanCloudId, ['CLOUD']);
238
		foreach ($addressBookEntries as $entry) {
239
			if (isset($entry['CLOUD'])) {
240
				foreach ($entry['CLOUD'] as $cloudID) {
241
					if ($cloudID === $cleanCloudId) {
242
						$response['UserFriendlyName'] = $entry['FN'];
243
						break;
244
					}
245
				}
246
			}
247
		}
248
		return $response;
249
	}
250
251
	private function shouldWatermark($isPublic, $userId, $fileId, Wopi $wopi) {
252
		if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_enabled', 'no') === 'no') {
253
			return false;
254
		}
255
256
		if ($isPublic) {
257
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkAll', 'no') === 'yes') {
258
				return true;
259
			}
260 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...
261
				return true;
262
			}
263
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkSecure', 'no') === 'yes' && $wopi->getHideDownload()) {
0 ignored issues
show
Documentation Bug introduced by
The method getHideDownload 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...
264
				return true;
265
			}
266 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...
267
				$tags = $this->appConfig->getAppValueArray('watermark_linkTagsList');
268
				$fileTags = \OC::$server->getSystemTagObjectMapper()->getTagIdsForObjects([$fileId], 'files')[$fileId];
269
				foreach ($fileTags as $tagId) {
270
					if (in_array($tagId, $tags, true)) {
271
						return true;
272
					}
273
				}
274
			}
275
		} else {
276
			if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareAll', 'no') === 'yes') {
277
				$files = $this->rootFolder->getUserFolder($userId)->getById($fileId);
278
				if (count($files) !== 0 && $files[0]->getOwner()->getUID() !== $userId) {
279
					return true;
280
				}
281
			}
282 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...
283
				return true;
284
			}
285
		}
286
		if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allGroups', 'no') === 'yes') {
287
			$groups = $this->appConfig->getAppValueArray('watermark_allGroupsList');
288
			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...
289
				if (\OC::$server->getGroupManager()->isInGroup($userId, $group)) {
290
					return true;
291
				}
292
			}
293
		}
294 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...
295
			$tags = $this->appConfig->getAppValueArray('watermark_allTagsList');
296
			$fileTags = \OC::$server->getSystemTagObjectMapper()->getTagIdsForObjects([$fileId], 'files')[$fileId];
297
			foreach ($fileTags as $tagId) {
298
				if (in_array($tagId, $tags, true)) {
299
					return true;
300
				}
301
			}
302
		}
303
304
		return false;
305
	}
306
307
	/**
308
	 * Given an access token and a fileId, returns the contents of the file.
309
	 * Expects a valid token in access_token parameter.
310
	 *
311
	 * @PublicPage
312
	 * @NoCSRFRequired
313
	 *
314
	 * @param string $fileId
315
	 * @param string $access_token
316
	 * @return Http\Response
317
	 * @throws DoesNotExistException
318
	 * @throws NotFoundException
319
	 * @throws NotPermittedException
320
	 */
321
	public function getFile($fileId,
322
							$access_token) {
323
		list($fileId, , $version) = Helper::parseFileId($fileId);
324
325
		$wopi = $this->wopiMapper->getWopiForToken($access_token);
326
327
		if ((int)$fileId !== $wopi->getFileid()) {
328
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
329
		}
330
331
		// Template is just returned as there is no version logic
332
		if ($wopi->isTemplateToken()) {
333
			$this->templateManager->setUserId($wopi->getOwnerUid());
334
			$file = $this->templateManager->get($wopi->getFileid());
335
			$response = new StreamResponse($file->fopen('rb'));
336
			$response->addHeader('Content-Disposition', 'attachment');
337
			$response->addHeader('Content-Type', 'application/octet-stream');
338
			return $response;
339
		}
340
341
		try {
342
			/** @var File $file */
343
			$userFolder = $this->rootFolder->getUserFolder($wopi->getOwnerUid());
344
			$file = $userFolder->getById($fileId)[0];
345
			\OC_User::setIncognitoMode(true);
346
			if ($version !== '0') {
347
				$view = new View('/' . $wopi->getOwnerUid() . '/files');
348
				$relPath = $view->getRelativePath($file->getPath());
349
				$versionPath = '/files_versions/' . $relPath . '.v' . $version;
350
				$view = new View('/' . $wopi->getOwnerUid());
351
				if ($view->file_exists($versionPath)){
352
					$response = new StreamResponse($view->fopen($versionPath, 'rb'));
353
				}
354
				else {
355
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
356
				}
357
			}
358
			else
359
			{
360
				$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...
361
			}
362
			$response->addHeader('Content-Disposition', 'attachment');
363
			$response->addHeader('Content-Type', 'application/octet-stream');
364
			return $response;
365
		} catch (\Exception $e) {
366
			$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...
367
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
368
		}
369
	}
370
371
	/**
372
	 * Given an access token and a fileId, replaces the files with the request body.
373
	 * Expects a valid token in access_token parameter.
374
	 *
375
	 * @PublicPage
376
	 * @NoCSRFRequired
377
	 *
378
	 * @param string $fileId
379
	 * @param string $access_token
380
	 * @return JSONResponse
381
	 * @throws DoesNotExistException
382
	 */
383
	public function putFile($fileId,
384
							$access_token) {
385
		list($fileId, ,) = Helper::parseFileId($fileId);
386
		$isPutRelative = ($this->request->getHeader('X-WOPI-Override') === 'PUT_RELATIVE');
387
		$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...
388
389
		$wopi = $this->wopiMapper->getWopiForToken($access_token);
390
		if (!$wopi->getCanwrite()) {
391
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
392
		}
393
394
		try {
395
			if ($isPutRelative) {
396
				// the new file needs to be installed in the current user dir
397
				$userFolder = $this->rootFolder->getUserFolder($wopi->getEditorUid());
398
				$file = $userFolder->getById($fileId);
399
				if (count($file) === 0) {
400
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
401
				}
402
				$file = $file[0];
403
				$suggested = $this->request->getHeader('X-WOPI-SuggestedTarget');
404
				$suggested = iconv('utf-7', 'utf-8', $suggested);
405
406 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...
407
					$path = dirname($file->getPath()) . '/New File' . $suggested;
408
				}
409
				else if ($suggested[0] !== '/') {
410
					$path = dirname($file->getPath()) . '/' . $suggested;
411
				}
412
				else {
413
					$path = $userFolder->getPath() . $suggested;
414
				}
415
416
				if ($path === '') {
417
					return new JSONResponse([
418
						'status' => 'error',
419
						'message' => 'Cannot create the file'
420
					]);
421
				}
422
423
				// create the folder first
424
				if (!$this->rootFolder->nodeExists(dirname($path))) {
425
					$this->rootFolder->newFolder(dirname($path));
426
				}
427
428
				// create a unique new file
429
				$path = $this->rootFolder->getNonExistingName($path);
430
				$this->rootFolder->newFile($path);
431
				$file = $this->rootFolder->get($path);
432
			} else {
433
				$file = $this->getFileForWopiToken($wopi);
434
				$wopiHeaderTime = $this->request->getHeader('X-LOOL-WOPI-Timestamp');
435
436
				if ($wopiHeaderTime !== null && $wopiHeaderTime !== Helper::toISO8601($file->getMTime() ?? 0)) {
437
					$this->logger->debug('Document timestamp mismatch ! WOPI client says mtime {headerTime} but storage says {storageTime}', [
438
						'headerTime' => $wopiHeaderTime,
439
						'storageTime' => Helper::toISO8601($file->getMTime() ?? 0)
440
					]);
441
					// Tell WOPI client about this conflict.
442
					return new JSONResponse(['LOOLStatusCode' => self::LOOL_STATUS_DOC_CHANGED], Http::STATUS_CONFLICT);
443
				}
444
			}
445
446
			$content = fopen('php://input', 'rb');
447
			// Set the user to register the change under his name
448
			$this->userScopeService->setUserScope($wopi->getEditorUid());
449
			$this->userScopeService->setFilesystemScope($isPutRelative ? $wopi->getEditorUid() : $wopi->getOwnerUid());
450
451
			try {
452
				$this->retryOperation(function () use ($file, $content){
453
					return $file->putContent($content);
454
				});
455
			} catch (LockedException $e) {
456
				$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...
457
				return new JSONResponse(['message' => 'File locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
458
			}
459
460
			if ($isPutRelative) {
461
				// generate a token for the new file (the user still has to be
462
				// logged in)
463
				list(, $wopiToken) = $this->tokenManager->getToken($file->getId(), null, $wopi->getEditorUid());
464
465
				$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
466
				$url = $this->urlGenerator->getAbsoluteURL($wopi);
467
468
				return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
469
			}
470
471
			return new JSONResponse(['LastModifiedTime' => Helper::toISO8601($file->getMTime())]);
472
		} catch (\Exception $e) {
473
			$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...
474
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
475
		}
476
	}
477
478
	/**
479
	 * Given an access token and a fileId, replaces the files with the request body.
480
	 * Expects a valid token in access_token parameter.
481
	 * Just actually routes to the PutFile, the implementation of PutFile
482
	 * handles both saving and saving as.* Given an access token and a fileId, replaces the files with the request body.
483
	 *
484
	 * FIXME Cleanup this code as is a lot of shared logic between putFile and putRelativeFile
485
	 *
486
	 * @PublicPage
487
	 * @NoCSRFRequired
488
	 *
489
	 * @param string $fileId
490
	 * @param string $access_token
491
	 * @return JSONResponse
492
	 * @throws DoesNotExistException
493
	 */
494
	public function putRelativeFile($fileId,
495
					$access_token) {
496
		list($fileId, ,) = Helper::parseFileId($fileId);
497
		$wopi = $this->wopiMapper->getWopiForToken($access_token);
498
		$isRenameFile = ($this->request->getHeader('X-WOPI-Override') === 'RENAME_FILE');
499
500
		if (!$wopi->getCanwrite()) {
501
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
502
		}
503
504
		// Unless the editor is empty (public link) we modify the files as the current editor
505
		$editor = $wopi->getEditorUid();
506
		if ($editor === null || $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...
507
			$editor = $wopi->getOwnerUid();
508
		}
509
510
		try {
511
			// the new file needs to be installed in the current user dir
512
			$userFolder = $this->rootFolder->getUserFolder($editor);
513
514
			if ($wopi->isTemplateToken()) {
515
				$this->templateManager->setUserId($wopi->getOwnerUid());
516
				$file = $userFolder->getById($wopi->getTemplateDestination())[0];
517
			} else if ($isRenameFile) {
518
				// the new file needs to be installed in the current user dir
519
				$userFolder = $this->rootFolder->getUserFolder($wopi->getEditorUid());
520
				$file = $userFolder->getById($fileId)[0];
521
522
				$suggested = $this->request->getHeader('X-WOPI-RequestedName');
523
524
				$suggested = iconv('utf-7', 'utf-8', $suggested) . '.' . $file->getExtension();
525
526
				if (strpos($suggested, '.') === 0) {
527
					$path = dirname($file->getPath()) . '/New File' . $suggested;
528
				}
529
				else if (strpos($suggested, '/') !== 0) {
530
					$path = dirname($file->getPath()) . '/' . $suggested;
531
				}
532
				else {
533
					$path = $userFolder->getPath() . $suggested;
534
				}
535
536
				if ($path === '') {
537
					return new JSONResponse([
538
						'status' => 'error',
539
						'message' => 'Cannot rename the file'
540
					]);
541
				}
542
543
				// create the folder first
544
				if (!$this->rootFolder->nodeExists(dirname($path))) {
545
					$this->rootFolder->newFolder(dirname($path));
546
				}
547
548
				// create a unique new file
549
				$path = $this->rootFolder->getNonExistingName($path);
550
				$file = $file->move($path);
551
			} else {
552
				$file = $userFolder->getById($fileId);
553
				if (count($file) === 0) {
554
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
555
				}
556
				$file = $file[0];
557
558
				$suggested = $this->request->getHeader('X-WOPI-SuggestedTarget');
559
				$suggested = iconv('utf-7', 'utf-8', $suggested);
560
561 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...
562
					$path = dirname($file->getPath()) . '/New File' . $suggested;
563
				} else if ($suggested[0] !== '/') {
564
					$path = dirname($file->getPath()) . '/' . $suggested;
565
				} else {
566
					$path = $userFolder->getPath() . $suggested;
567
				}
568
569
				if ($path === '') {
570
					return new JSONResponse([
571
						'status' => 'error',
572
						'message' => 'Cannot create the file'
573
					]);
574
				}
575
576
				// create the folder first
577
				if (!$this->rootFolder->nodeExists(dirname($path))) {
578
					$this->rootFolder->newFolder(dirname($path));
579
				}
580
581
				// create a unique new file
582
				$path = $this->rootFolder->getNonExistingName($path);
583
				$file = $this->rootFolder->newFile($path);
584
			}
585
586
			$content = fopen('php://input', 'rb');
587
			// Set the user to register the change under his name
588
			$this->userScopeService->setUserScope($wopi->getEditorUid());
589
			$this->userScopeService->setFilesystemScope($isPutRelative ? $wopi->getEditorUid() : $wopi->getOwnerUid());
0 ignored issues
show
Bug introduced by
The variable $isPutRelative does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
590
591
			try {
592
				$this->retryOperation(function () use ($file, $content){
593
					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...
594
				});
595
			} catch (LockedException $e) {
596
				return new JSONResponse(['message' => 'File locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
597
			}
598
599
			// generate a token for the new file (the user still has to be
600
			// logged in)
601
			list(, $wopiToken) = $this->tokenManager->getToken($file->getId(), null, $wopi->getEditorUid());
602
603
			$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
604
			$url = $this->urlGenerator->getAbsoluteURL($wopi);
605
606
			return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
607
		} catch (\Exception $e) {
608
			$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...
609
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
610
		}
611
	}
612
613
	/**
614
	 * Retry operation if a LockedException occurred
615
	 * Other exceptions will still be thrown
616
	 * @param callable $operation
617
	 * @throws LockedException
618
	 * @throws GenericFileException
619
	 */
620
	private function retryOperation(callable $operation) {
621
		for ($i = 0; $i < 5; $i++) {
622
			try {
623
				if ($operation() !== false) {
624
					return;
625
				}
626
			} catch (LockedException $e) {
627
				if ($i === 4) {
628
					throw $e;
629
				}
630
				usleep(500000);
631
			}
632
		}
633
		throw new GenericFileException('Operation failed after multiple retries');
634
	}
635
636
	/**
637
	 * @param Wopi $wopi
638
	 * @return File|Folder|Node|null
639
	 * @throws NotFoundException
640
	 * @throws ShareNotFound
641
	 */
642
	private function getFileForWopiToken(Wopi $wopi) {
643
		$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...
644
645
		if ($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...
646
			$share = $this->shareManager->getShareByToken($wopi->getEditorUid());
647
			$node = $share->getNode();
648
			if ($node instanceof Folder) {
649
				$file = $node->getById($wopi->getFileid())[0];
650
			} else {
651
				$file = $node;
652
			}
653
		} else {
654
			// Unless the editor is empty (public link) we modify the files as the current editor
655
			// TODO: add related share token to the wopi table so we can obtain the
656
			$editor = $wopi->getEditorUid();
657
658
			// Use the actual file owner no editor is available
659
			if ($editor === null || $wopi->getGuestDisplayname() === null) {
660
				$editor = $wopi->getOwnerUid();
661
			}
662
663
			$userFolder = $this->rootFolder->getUserFolder($editor);
664
			$files = $userFolder->getById($wopi->getFileid());
665
			if (isset($files[0]) && $files[0] instanceof File) {
666
				$file = $files[0];
667
			} else {
668
				throw new NotFoundException('No valid file found for wopi token');
669
			}
670
		}
671
		return $file;
672
	}
673
674
	/**
675
	 * Endpoint to return the template file that is requested by collabora to create a new document
676
	 *
677
	 * @PublicPage
678
	 * @NoCSRFRequired
679
	 *
680
	 * @param $fileId
681
	 * @param $access_token
682
	 * @return JSONResponse|StreamResponse
683
	 */
684
	public function getTemplate($fileId, $access_token) {
685
		try {
686
			$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...
687
		} catch (DoesNotExistException $e) {
688
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
689
		}
690
691
		if ((int)$fileId !== $wopi->getTemplateId()) {
692
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
693
		}
694
695
		try {
696
			$this->templateManager->setUserId($wopi->getOwnerUid());
697
			$file = $this->templateManager->get($wopi->getTemplateId());
698
			$response = new StreamResponse($file->fopen('rb'));
699
			$response->addHeader('Content-Disposition', 'attachment');
700
			$response->addHeader('Content-Type', 'application/octet-stream');
701
			return $response;
702
		} catch (\Exception $e) {
703
			$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...
704
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
705
		}
706
	}
707
708
}
709