Completed
Push — master ( ca238d...2c3d9a )
by Andras
02:15
created

WopiController::putRelativeFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
crap 2
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\TokenManager;
26
use OCA\Richdocuments\Db\Wopi;
27
use OCA\Richdocuments\Helper;
28
use OCP\AppFramework\Controller;
29
use OCP\AppFramework\Http;
30
use OCP\AppFramework\Http\JSONResponse;
31
use OCP\Files\File;
32
use OCP\Files\IRootFolder;
33
use OCP\IConfig;
34
use OCP\IRequest;
35
use OCP\IURLGenerator;
36
use OCP\AppFramework\Http\StreamResponse;
37
use OCP\IUserManager;
38
39
class WopiController extends Controller {
40
	/** @var IRootFolder */
41
	private $rootFolder;
42
	/** @var IURLGenerator */
43
	private $urlGenerator;
44
	/** @var IConfig */
45
	private $config;
46
	/** @var ITokenManager */
47
	private $tokenManager;
48
	/** @var IUserManager */
49
	private $userManager;
50
51
	// Signifies LOOL that document has been changed externally in this storage
52
	const LOOL_STATUS_DOC_CHANGED = 1010;
53
54
	/**
55
	 * @param string $appName
56
	 * @param string $UserId
57
	 * @param IRequest $request
58
	 * @param IRootFolder $rootFolder
59
	 * @param IURLGenerator $urlGenerator
60
	 * @param IConfig $config
61
	 * @param ITokenManager $tokenManager
62
	 * @param IUserManager $userManager
63
	 */
64
	public function __construct($appName,
65
								$UserId,
66
								IRequest $request,
67
								IRootFolder $rootFolder,
68
								IURLGenerator $urlGenerator,
69
								IConfig $config,
70
								TokenManager $tokenManager,
71
								IUserManager $userManager) {
72
		parent::__construct($appName, $request);
73
		$this->rootFolder = $rootFolder;
74
		$this->urlGenerator = $urlGenerator;
75
		$this->config = $config;
76
		$this->tokenManager = $tokenManager;
0 ignored issues
show
Documentation Bug introduced by
It seems like $tokenManager of type object<OCA\Richdocuments\TokenManager> is incompatible with the declared type object<OCA\Richdocuments...ntroller\ITokenManager> of property $tokenManager.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
77
		$this->userManager = $userManager;
78
	}
79
80
	/**
81
	 * Returns general info about a file.
82
	 *
83
	 * @NoAdminRequired
84
	 * @NoCSRFRequired
85
	 * @PublicPage
86
	 *
87
	 * @param string $fileId
88
	 * @return JSONResponse
89
	 */
90
	public function checkFileInfo($fileId) {
91
		$token = $this->request->getParam('access_token');
92
93
		list($fileId, , $version) = Helper::parseFileId($fileId);
94
		$db = new Wopi();
95
		$res = $db->getPathForToken($fileId, $token);
96
		if ($res === false) {
97
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
98
		}
99
100
		// Login the user to see his mount locations
101
		try {
102
			/** @var File $file */
103
			$userFolder = $this->rootFolder->getUserFolder($res['owner']);
104
			$file = $userFolder->getById($fileId)[0];
105
		} catch (\Exception $e) {
106
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
107
		}
108
109
		if(!($file instanceof File)) {
0 ignored issues
show
Bug introduced by
The class OCP\Files\File does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
110
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
111
		}
112
113
		$response = [
114
			'BaseFileName' => $file->getName(),
115
			'Size' => $file->getSize(),
116
			'Version' => $version,
117
			'UserId' => $res['editor'] !== '' ? $res['editor'] : 'Guest user',
118
			'OwnerId' => $res['owner'],
119
			'UserFriendlyName' => $res['editor'] !== '' ? \OC_User::getDisplayName($res['editor']) : 'Guest user',
120
			'UserExtraInfo' => [
121
			],
122
			'UserCanWrite' => $res['canwrite'] ? true : false,
123
			'UserCanNotWriteRelative' => \OC::$server->getEncryptionManager()->isEnabled() ? true : false,
124
			'PostMessageOrigin' => $res['server_host'],
125
			'LastModifiedTime' => Helper::toISO8601($file->getMtime())
126
		];
127
128
		$serverVersion = $this->config->getSystemValue('version');
129
		if (version_compare($serverVersion, '13', '>=')) {
130
			$user = $this->userManager->get($res['editor']);
131
			if($user !== null) {
132
				if($user->getAvatarImage(32) !== null) {
133
					$response['UserExtraInfo']['avatar'] = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $res['editor'], 'size' => 32]);
134
				}
135
			}
136
		}
137
138
		return new JSONResponse($response);
139
	}
140
141
	/**
142
	 * Given an access token and a fileId, returns the contents of the file.
143
	 * Expects a valid token in access_token parameter.
144
	 *
145
	 * @PublicPage
146
	 * @NoCSRFRequired
147
	 *
148
	 * @param string $fileId
149
	 * @param string $access_token
150
	 * @return Http\Response
151
	 */
152
	public function getFile($fileId,
153
							$access_token) {
154
		list($fileId, , $version) = Helper::parseFileId($fileId);
155
		$row = new Wopi();
156
		$row->loadBy('token', $access_token);
157
		$res = $row->getPathForToken($fileId, $access_token);
158
		try {
159
			/** @var File $file */
160
			$userFolder = $this->rootFolder->getUserFolder($res['owner']);
161
			$file = $userFolder->getById($fileId)[0];
162
			\OC_User::setIncognitoMode(true);
163
			if ($version !== '0')
164
			{
165
				$view = new View('/' . $res['owner'] . '/files');
166
				$relPath = $view->getRelativePath($file->getPath());
167
				$versionPath = '/files_versions/' . $relPath . '.v' . $version;
168
				$view = new View('/' . $res['owner']);
169
				if ($view->file_exists($versionPath)){
170
					$response = new StreamResponse($view->fopen($versionPath, 'rb'));
171
				}
172
				else {
173
					$response->setStatus(Http::STATUS_NOT_FOUND);
0 ignored issues
show
Bug introduced by
The variable $response seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
174
				}
175
			}
176
			else
177
			{
178
				$response = new StreamResponse($file->fopen('rb'));
179
			}
180
			$response->addHeader('Content-Disposition', 'attachment');
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
181
			$response->addHeader('Content-Type', 'application/octet-stream');
182
			return $response;
183
		} catch (\Exception $e) {
184
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
185
		}
186
	}
187
188
	/**
189
	 * Given an access token and a fileId, replaces the files with the request body.
190
	 * Expects a valid token in access_token parameter.
191
	 *
192
	 * @PublicPage
193
	 * @NoCSRFRequired
194
	 *
195
	 * @param string $fileId
196
	 * @param string $access_token
197
	 * @return JSONResponse
198
	 */
199
	public function putFile($fileId,
200
							$access_token) {
201
		list($fileId, , $version) = Helper::parseFileId($fileId);
0 ignored issues
show
Unused Code introduced by
The assignment to $version is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
202
		$isPutRelative = ($this->request->getHeader('X-WOPI-Override') === 'PUT_RELATIVE');
203
204
		$row = new Wopi();
205
		$row->loadBy('token', $access_token);
206
207
		$res = $row->getPathForToken($fileId, $access_token);
208
		if (!$res['canwrite']) {
209
			return new JSONResponse([], Http::STATUS_FORBIDDEN);
210
		}
211
212
		try {
213
			/** @var File $file */
214
			$userFolder = $this->rootFolder->getUserFolder($res['owner']);
215
			$file = $userFolder->getById($fileId)[0];
216
217
			if ($isPutRelative) {
218
				$suggested = $this->request->getHeader('X-WOPI-SuggestedTarget');
219
				$suggested = iconv('utf-7', 'utf-8', $suggested);
220
221
				$path = '';
222
				if ($suggested[0] === '.') {
223
					$path = dirname($file->getPath()) . '/New File' . $suggested;
224
				}
225
				else if ($suggested[0] !== '/') {
226
					$path = dirname($file->getPath()) . '/' . $suggested;
227
				}
228
				else {
229
					$path = $userFolder->getPath() . $suggested;
230
				}
231
232
				if ($path === '') {
233
					return array(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('status' =>...nnot create the file'); (array<string,string>) is incompatible with the return type documented by OCA\Richdocuments\Contro...WopiController::putFile of type OCP\AppFramework\Http\JSONResponse.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
234
						'status' => 'error',
235
						'message' => 'Cannot create the file'
236
					);
237
				}
238
239
				$root = \OC::$server->getRootFolder();
240
241
				// create the folder first
242
				if (!$root->nodeExists(dirname($path))) {
243
					$root->newFolder(dirname($path));
244
				}
245
246
				// create a unique new file
247
				$path = $root->getNonExistingName($path);
248
				$root->newFile($path);
249
				$file = $root->get($path);
250
			}
251
			else {
252
				$wopiHeaderTime = $this->request->getHeader('X-LOOL-WOPI-Timestamp');
253
				if (!is_null($wopiHeaderTime) && $wopiHeaderTime != Helper::toISO8601($file->getMTime())) {
254
					\OC::$server->getLogger()->debug('Document timestamp mismatch ! WOPI client says mtime {headerTime} but storage says {storageTime}', [
255
						'headerTime' => $wopiHeaderTime,
256
						'storageTime' => Helper::toISO8601($file->getMtime())
257
					]);
258
					// Tell WOPI client about this conflict.
259
					return new JSONResponse(['LOOLStatusCode' => self::LOOL_STATUS_DOC_CHANGED], Http::STATUS_CONFLICT);
260
				}
261
			}
262
263
			$content = fopen('php://input', 'rb');
264
			// Setup the FS which is needed to emit hooks (versioning).
265
			\OC_Util::tearDownFS();
266
			\OC_Util::setupFS($res['owner']);
267
268
			// Set the user to register the change under his name
269
			$editor = \OC::$server->getUserManager()->get($res['editor']);
270
			if (!is_null($editor)) {
271
				\OC::$server->getUserSession()->setUser($editor);
272
			}
273
274
			$file->putContent($content);
275
276
			if ($isPutRelative) {
277
				// generate a token for the new file (the user still has to be
278
				// logged in)
279
				$serverHost = $this->request->getServerProtocol() . '://' . $this->request->getServerHost();
0 ignored issues
show
Unused Code introduced by
$serverHost 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...
280
				list(, $wopiToken) = $this->tokenManager->getToken($file->getId());
281
282
				$wopi = 'index.php/apps/richdocuments/wopi/files/' . $file->getId() . '_' . $this->config->getSystemValue('instanceid') . '?access_token=' . $wopiToken;
283
				$url = \OC::$server->getURLGenerator()->getAbsoluteURL($wopi);
284
285
				return new JSONResponse([ 'Name' => $file->getName(), 'Url' => $url ], Http::STATUS_OK);
286
			}
287
			else {
288
				return new JSONResponse(['LastModifiedTime' => Helper::toISO8601($file->getMtime())]);
289
			}
290
		} catch (\Exception $e) {
291
			return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
292
		}
293
	}
294
295
	/**
296
	 * Given an access token and a fileId, replaces the files with the request body.
297
	 * Expects a valid token in access_token parameter.
298
	 * Just actually routes to the PutFile, the implementation of PutFile
299
	 * handles both saving and saving as.* Given an access token and a fileId, replaces the files with the request body.
300
	 *
301
	 * @PublicPage
302
	 * @NoCSRFRequired
303
	 *
304
	 * @param string $fileId
305
	 * @param string $access_token
306
	 * @return JSONResponse
307
	 */
308
	public function putRelativeFile($fileId,
309
					$access_token) {
310
		return $this->putFile($fileId, $access_token);
311
	}
312
}
313