Completed
Push — master ( 1dcb4e...f2b2f2 )
by Julius
02:26 queued 14s
created

WopiController   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 652
Duplicated Lines 6.13 %

Coupling/Cohesion

Components 1
Dependencies 26

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 96
lcom 1
cbo 26
dl 40
loc 652
ccs 0
cts 445
cp 0
rs 1.948
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 28 1
F checkFileInfo() 0 95 16
A setFederationFileInfo() 0 19 5
D shouldWatermark() 24 55 22
B getFile() 0 49 6
D putFile() 9 98 15
F putRelativeFile() 7 118 17
A retryOperation() 0 15 5
A getFileForWopiToken() 0 24 5
A getTemplate() 0 23 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WopiController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WopiController, and based on these observations, apply Extract Interface, too.

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
		// Set the user to register the change under his name
395
		$this->userScopeService->setUserScope($wopi->getEditorUid());
396
		$this->userScopeService->setFilesystemScope($isPutRelative ? $wopi->getEditorUid() : $wopi->getUserForFileAccess());
397
398
		try {
399
			if ($isPutRelative) {
400
				// the new file needs to be installed in the current user dir
401
				$userFolder = $this->rootFolder->getUserFolder($wopi->getEditorUid());
402
				$file = $userFolder->getById($fileId);
403
				if (count($file) === 0) {
404
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
405
				}
406
				$file = $file[0];
407
				$suggested = $this->request->getHeader('X-WOPI-SuggestedTarget');
408
				$suggested = iconv('utf-7', 'utf-8', $suggested);
409
410 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...
411
					$path = dirname($file->getPath()) . '/New File' . $suggested;
412
				}
413
				else if ($suggested[0] !== '/') {
414
					$path = dirname($file->getPath()) . '/' . $suggested;
415
				}
416
				else {
417
					$path = $userFolder->getPath() . $suggested;
418
				}
419
420
				if ($path === '') {
421
					return new JSONResponse([
422
						'status' => 'error',
423
						'message' => 'Cannot create the file'
424
					]);
425
				}
426
427
				// create the folder first
428
				if (!$this->rootFolder->nodeExists(dirname($path))) {
429
					$this->rootFolder->newFolder(dirname($path));
430
				}
431
432
				// create a unique new file
433
				$path = $this->rootFolder->getNonExistingName($path);
434
				$this->rootFolder->newFile($path);
435
				$file = $this->rootFolder->get($path);
436
			} else {
437
				$file = $this->getFileForWopiToken($wopi);
438
				$wopiHeaderTime = $this->request->getHeader('X-LOOL-WOPI-Timestamp');
439
440
				if ($wopiHeaderTime !== null && $wopiHeaderTime !== Helper::toISO8601($file->getMTime() ?? 0)) {
441
					$this->logger->debug('Document timestamp mismatch ! WOPI client says mtime {headerTime} but storage says {storageTime}', [
442
						'headerTime' => $wopiHeaderTime,
443
						'storageTime' => Helper::toISO8601($file->getMTime() ?? 0)
444
					]);
445
					// Tell WOPI client about this conflict.
446
					return new JSONResponse(['LOOLStatusCode' => self::LOOL_STATUS_DOC_CHANGED], Http::STATUS_CONFLICT);
447
				}
448
			}
449
450
			$content = fopen('php://input', 'rb');
451
452
			try {
453
				$this->retryOperation(function () use ($file, $content){
454
					return $file->putContent($content);
455
				});
456
			} catch (LockedException $e) {
457
				$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...
458
				return new JSONResponse(['message' => 'File locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
459
			}
460
461
			if ($isPutRelative) {
462
				// generate a token for the new file (the user still has to be
463
				// logged in)
464
				list(, $wopiToken) = $this->tokenManager->getToken($file->getId(), null, $wopi->getEditorUid());
465
466
				$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
467
				$url = $this->urlGenerator->getAbsoluteURL($wopi);
468
469
				return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
470
			}
471
			if ($wopi->hasTemplateId()) {
472
				$wopi->setTemplateId(null);
473
				$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...
474
			}
475
			return new JSONResponse(['LastModifiedTime' => Helper::toISO8601($file->getMTime())]);
476
		} catch (\Exception $e) {
477
			$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...
478
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
479
		}
480
	}
481
482
	/**
483
	 * Given an access token and a fileId, replaces the files with the request body.
484
	 * Expects a valid token in access_token parameter.
485
	 * Just actually routes to the PutFile, the implementation of PutFile
486
	 * handles both saving and saving as.* Given an access token and a fileId, replaces the files with the request body.
487
	 *
488
	 * FIXME Cleanup this code as is a lot of shared logic between putFile and putRelativeFile
489
	 *
490
	 * @PublicPage
491
	 * @NoCSRFRequired
492
	 *
493
	 * @param string $fileId
494
	 * @param string $access_token
495
	 * @return JSONResponse
496
	 * @throws DoesNotExistException
497
	 */
498
	public function putRelativeFile($fileId,
499
					$access_token) {
500
		list($fileId, ,) = Helper::parseFileId($fileId);
501
		$wopi = $this->wopiMapper->getWopiForToken($access_token);
502
		$isRenameFile = ($this->request->getHeader('X-WOPI-Override') === 'RENAME_FILE');
503
504
		if (!$wopi->getCanwrite()) {
505
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
506
		}
507
508
		// Unless the editor is empty (public link) we modify the files as the current editor
509
		$editor = $wopi->getEditorUid();
510
		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...
511
			$editor = $wopi->getOwnerUid();
512
		}
513
514
		try {
515
			// the new file needs to be installed in the current user dir
516
			$userFolder = $this->rootFolder->getUserFolder($editor);
517
518
			if ($wopi->isTemplateToken()) {
519
				$this->templateManager->setUserId($wopi->getOwnerUid());
520
				$file = $userFolder->getById($wopi->getTemplateDestination())[0];
521
			} else if ($isRenameFile) {
522
				// the new file needs to be installed in the current user dir
523
				$userFolder = $this->rootFolder->getUserFolder($wopi->getEditorUid());
524
				$file = $userFolder->getById($fileId)[0];
525
526
				$suggested = $this->request->getHeader('X-WOPI-RequestedName');
527
528
				$suggested = iconv('utf-7', 'utf-8', $suggested) . '.' . $file->getExtension();
529
530
				if (strpos($suggested, '.') === 0) {
531
					$path = dirname($file->getPath()) . '/New File' . $suggested;
532
				}
533
				else if (strpos($suggested, '/') !== 0) {
534
					$path = dirname($file->getPath()) . '/' . $suggested;
535
				}
536
				else {
537
					$path = $userFolder->getPath() . $suggested;
538
				}
539
540
				if ($path === '') {
541
					return new JSONResponse([
542
						'status' => 'error',
543
						'message' => 'Cannot rename the file'
544
					]);
545
				}
546
547
				// create the folder first
548
				if (!$this->rootFolder->nodeExists(dirname($path))) {
549
					$this->rootFolder->newFolder(dirname($path));
550
				}
551
552
				// create a unique new file
553
				$path = $this->rootFolder->getNonExistingName($path);
554
				$file = $file->move($path);
555
			} else {
556
				$file = $userFolder->getById($fileId);
557
				if (count($file) === 0) {
558
					return new JSONResponse([], Http::STATUS_NOT_FOUND);
559
				}
560
				$file = $file[0];
561
562
				$suggested = $this->request->getHeader('X-WOPI-SuggestedTarget');
563
				$suggested = iconv('utf-7', 'utf-8', $suggested);
564
565 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...
566
					$path = dirname($file->getPath()) . '/New File' . $suggested;
567
				} else if ($suggested[0] !== '/') {
568
					$path = dirname($file->getPath()) . '/' . $suggested;
569
				} else {
570
					$path = $userFolder->getPath() . $suggested;
571
				}
572
573
				if ($path === '') {
574
					return new JSONResponse([
575
						'status' => 'error',
576
						'message' => 'Cannot create the file'
577
					]);
578
				}
579
580
				// create the folder first
581
				if (!$this->rootFolder->nodeExists(dirname($path))) {
582
					$this->rootFolder->newFolder(dirname($path));
583
				}
584
585
				// create a unique new file
586
				$path = $this->rootFolder->getNonExistingName($path);
587
				$file = $this->rootFolder->newFile($path);
588
			}
589
590
			$content = fopen('php://input', 'rb');
591
			// Set the user to register the change under his name
592
			$this->userScopeService->setUserScope($wopi->getEditorUid());
593
			$this->userScopeService->setFilesystemScope($wopi->getEditorUid());
594
595
			try {
596
				$this->retryOperation(function () use ($file, $content){
597
					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...
598
				});
599
			} catch (LockedException $e) {
600
				return new JSONResponse(['message' => 'File locked'], Http::STATUS_INTERNAL_SERVER_ERROR);
601
			}
602
603
			// generate a token for the new file (the user still has to be
604
			// logged in)
605
			list(, $wopiToken) = $this->tokenManager->getToken($file->getId(), null, $wopi->getEditorUid());
606
607
			$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
608
			$url = $this->urlGenerator->getAbsoluteURL($wopi);
609
610
			return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
611
		} catch (\Exception $e) {
612
			$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...
613
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
614
		}
615
	}
616
617
	/**
618
	 * Retry operation if a LockedException occurred
619
	 * Other exceptions will still be thrown
620
	 * @param callable $operation
621
	 * @throws LockedException
622
	 * @throws GenericFileException
623
	 */
624
	private function retryOperation(callable $operation) {
625
		for ($i = 0; $i < 5; $i++) {
626
			try {
627
				if ($operation() !== false) {
628
					return;
629
				}
630
			} catch (LockedException $e) {
631
				if ($i === 4) {
632
					throw $e;
633
				}
634
				usleep(500000);
635
			}
636
		}
637
		throw new GenericFileException('Operation failed after multiple retries');
638
	}
639
640
	/**
641
	 * @param Wopi $wopi
642
	 * @return File|Folder|Node|null
643
	 * @throws NotFoundException
644
	 * @throws ShareNotFound
645
	 */
646
	private function getFileForWopiToken(Wopi $wopi) {
647
		$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...
648
649
		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...
650
			$share = $this->shareManager->getShareByToken($wopi->getEditorUid());
651
			$node = $share->getNode();
652
			if ($node instanceof Folder) {
653
				$file = $node->getById($wopi->getFileid())[0];
654
			} else {
655
				$file = $node;
656
			}
657
		} else {
658
			// Unless the editor is empty (public link) we modify the files as the current editor
659
			// TODO: add related share token to the wopi table so we can obtain the
660
			$userFolder = $this->rootFolder->getUserFolder($wopi->getUserForFileAccess());
661
			$files = $userFolder->getById($wopi->getFileid());
662
			if (isset($files[0]) && $files[0] instanceof File) {
663
				$file = $files[0];
664
			} else {
665
				throw new NotFoundException('No valid file found for wopi token');
666
			}
667
		}
668
		return $file;
669
	}
670
671
	/**
672
	 * Endpoint to return the template file that is requested by collabora to create a new document
673
	 *
674
	 * @PublicPage
675
	 * @NoCSRFRequired
676
	 *
677
	 * @param $fileId
678
	 * @param $access_token
679
	 * @return JSONResponse|StreamResponse
680
	 */
681
	public function getTemplate($fileId, $access_token) {
682
		try {
683
			$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...
684
		} catch (DoesNotExistException $e) {
685
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
686
		}
687
688
		if ((int)$fileId !== $wopi->getTemplateId()) {
689
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
690
		}
691
692
		try {
693
			$this->templateManager->setUserId($wopi->getOwnerUid());
694
			$file = $this->templateManager->get($wopi->getTemplateId());
695
			$response = new StreamResponse($file->fopen('rb'));
696
			$response->addHeader('Content-Disposition', 'attachment');
697
			$response->addHeader('Content-Type', 'application/octet-stream');
698
			return $response;
699
		} catch (\Exception $e) {
700
			$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...
701
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
702
		}
703
	}
704
705
}
706