Skip to content

Commit d7e035a

Browse files
committed
Merge branch 'feature/user-subdomains' into prep-release-0.10
2 parents cca825d + 61640e4 commit d7e035a

17 files changed

+662
-171
lines changed

site.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<VirtualHost *:443>
2+
# To use User SubDomains, make sure to add a "catch-all", for instance:
3+
# ServerName nextcloud.local
4+
# ServerAlias *.nextcloud.local
5+
26
DocumentRoot /var/www/html
37
ErrorLog ${APACHE_LOG_DIR}/error.log
48
CustomLog ${APACHE_LOG_DIR}/access.log combined

solid/appinfo/routes.php

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
<?php
2+
3+
use OC\AllConfig;
4+
use OC\AppConfig;
5+
use OCA\Solid\AppInfo\Application;
6+
use OCP\IConfig;
7+
use OCP\IRequest;
8+
29
/**
310
* Create your routes in here. The name is the lowercase name of the controller
411
* without the controller part, the stuff after the hash is the method.
@@ -7,58 +14,88 @@
714
* The controller class has to be registered in the application.php file since
815
* it's instantiated in there
916
*/
17+
18+
19+
$routes = [
20+
['name' => 'page#approval', 'url' => '/sharing/{clientId}', 'verb' => 'GET'],
21+
['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'DELETE'],
22+
['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'POST'],
23+
24+
['name' => 'page#handleApproval', 'url' => '/sharing/{clientId}', 'verb' => 'POST'],
25+
['name' => 'page#customscheme', 'url' => '/customscheme', 'verb' => 'GET'],
26+
27+
['name' => 'server#cors', 'url' => '/{path}', 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
28+
['name' => 'server#authorize', 'url' => '/authorize', 'verb' => 'GET'],
29+
['name' => 'server#jwks', 'url' => '/jwks', 'verb' => 'GET'],
30+
['name' => 'server#session', 'url' => '/session', 'verb' => 'GET'],
31+
['name' => 'server#logout', 'url' => '/logout', 'verb' => 'GET'],
32+
['name' => 'server#token', 'url' => '/token', 'verb' => 'POST'],
33+
['name' => 'server#userinfo', 'url' => '/userinfo', 'verb' => 'GET'],
34+
['name' => 'server#register', 'url' => '/register', 'verb' => 'POST'],
35+
['name' => 'server#registeredClient', 'url' => '/register/{clientId}', 'verb' => 'GET'],
36+
37+
['name' => 'solidWebhook#listWebhooks', 'url' => '/webhook/list', 'verb' => 'GET'],
38+
['name' => 'solidWebhook#register', 'url' => '/webhook/register', 'verb' => 'POST'],
39+
['name' => 'solidWebhook#unregister', 'url' => '/webhook/unregister', 'verb' => 'POST'],
40+
41+
['name' => 'solidWebsocket#register', 'url' => '/websocket/register', 'verb' => 'POST'],
42+
43+
['name' => 'app#appLauncher', 'url' => '/', 'verb' => 'GET'],
44+
];
45+
46+
$userIdRoutes = [
47+
['name' => 'page#profile', 'url' => '/@{userId}/', 'verb' => 'GET'],
48+
49+
['name' => 'profile#handleGet', 'url' => '/@{userId}/profile{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ],
50+
['name' => 'profile#handlePut', 'url' => '/@{userId}/profile{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ],
51+
['name' => 'profile#handlePatch', 'url' => '/@{userId}/profile{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ],
52+
['name' => 'profile#handleHead', 'url' => '/@{userId}/profile{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ],
53+
54+
['name' => 'storage#handleGet', 'url' => '/@{userId}/storage{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ],
55+
['name' => 'storage#handlePost', 'url' => '/@{userId}/storage{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ],
56+
['name' => 'storage#handlePut', 'url' => '/@{userId}/storage{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ],
57+
['name' => 'storage#handleDelete', 'url' => '/@{userId}/storage{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ],
58+
['name' => 'storage#handlePatch', 'url' => '/@{userId}/storage{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ],
59+
['name' => 'storage#handleHead', 'url' => '/@{userId}/storage{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ],
60+
61+
['name' => 'calendar#handleGet', 'url' => '/@{userId}/calendar{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ],
62+
['name' => 'calendar#handlePost', 'url' => '/@{userId}/calendar{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ],
63+
['name' => 'calendar#handlePut', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ],
64+
['name' => 'calendar#handleDelete', 'url' => '/@{userId}/calendar{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ],
65+
['name' => 'calendar#handlePatch', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ],
66+
['name' => 'calendar#handleHead', 'url' => '/@{userId}/calendar{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ],
67+
68+
['name' => 'contacts#handleGet', 'url' => '/@{userId}/contacts{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ],
69+
['name' => 'contacts#handlePost', 'url' => '/@{userId}/contacts{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ],
70+
['name' => 'contacts#handlePut', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ],
71+
['name' => 'contacts#handleDelete', 'url' => '/@{userId}/contacts{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ],
72+
['name' => 'contacts#handlePatch', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ],
73+
['name' => 'contacts#handleHead', 'url' => '/@{userId}/contacts{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ],
74+
];
75+
76+
// @TODO: All routes NOT generated by the UrlGenerator ANYWHERE in the code need to be checked!
77+
78+
if (Application::$userSubDomainsEnabled) {
79+
$userIdRoutes = array_map(function ($route) {
80+
if ($route['name'] === 'page#profile') {
81+
// The profile route should be `/me` instead of `/@{userId}/`
82+
$route['url'] = '/me';
83+
} else {
84+
// When UserSubDomains are enabled, all routes that start with
85+
// `/@{userId}/` should just be `/`, as the userId is present
86+
// in the subdomain.
87+
$route['url'] = preg_replace('#^/@{userId}/#', '/', $route['url']);
88+
}
89+
90+
// The required userId is set to the userId from the subdomain
91+
$host = OC::$server->get(IRequest::class)->getServerHost();
92+
$userId = explode('.', $host)[0];
93+
$route['defaults'] = ['userId' => $userId];
94+
95+
return $route;
96+
}, $userIdRoutes);
97+
}
98+
1099
return [
11-
'routes' => [
12-
['name' => 'page#profile', 'url' => '/@{userId}/', 'verb' => 'GET'],
13-
['name' => 'page#approval', 'url' => '/sharing/{clientId}', 'verb' => 'GET'],
14-
['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'DELETE'],
15-
['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'POST'],
16-
17-
['name' => 'page#handleApproval', 'url' => '/sharing/{clientId}', 'verb' => 'POST'],
18-
['name' => 'page#customscheme', 'url' => '/customscheme', 'verb' => 'GET'],
19-
20-
['name' => 'server#cors', 'url' => '/{path}', 'verb' => 'OPTIONS', 'requirements' => array('path' => '.+') ],
21-
['name' => 'server#authorize', 'url' => '/authorize', 'verb' => 'GET'],
22-
['name' => 'server#jwks', 'url' => '/jwks', 'verb' => 'GET'],
23-
['name' => 'server#session', 'url' => '/session', 'verb' => 'GET'],
24-
['name' => 'server#logout', 'url' => '/logout', 'verb' => 'GET'],
25-
['name' => 'server#token', 'url' => '/token', 'verb' => 'POST'],
26-
['name' => 'server#userinfo', 'url' => '/userinfo', 'verb' => 'GET'],
27-
['name' => 'server#register', 'url' => '/register', 'verb' => 'POST'],
28-
['name' => 'server#registeredClient', 'url' => '/register/{clientId}', 'verb' => 'GET'],
29-
30-
['name' => 'profile#handleGet', 'url' => '/@{userId}/profile{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')],
31-
['name' => 'profile#handlePut', 'url' => '/@{userId}/profile{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')],
32-
['name' => 'profile#handlePatch', 'url' => '/@{userId}/profile{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')],
33-
['name' => 'profile#handleHead', 'url' => '/@{userId}/profile{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')],
34-
35-
['name' => 'storage#handleGet', 'url' => '/@{userId}/storage{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')],
36-
['name' => 'storage#handlePost', 'url' => '/@{userId}/storage{path}', 'verb' => 'POST', 'requirements' => array('path' => '.+')],
37-
['name' => 'storage#handlePut', 'url' => '/@{userId}/storage{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')],
38-
['name' => 'storage#handleDelete', 'url' => '/@{userId}/storage{path}', 'verb' => 'DELETE', 'requirements' => array('path' => '.+')],
39-
['name' => 'storage#handlePatch', 'url' => '/@{userId}/storage{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')],
40-
['name' => 'storage#handleHead', 'url' => '/@{userId}/storage{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')],
41-
42-
['name' => 'calendar#handleGet', 'url' => '/@{userId}/calendar{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')],
43-
['name' => 'calendar#handlePost', 'url' => '/@{userId}/calendar{path}', 'verb' => 'POST', 'requirements' => array('path' => '.+')],
44-
['name' => 'calendar#handlePut', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')],
45-
['name' => 'calendar#handleDelete', 'url' => '/@{userId}/calendar{path}', 'verb' => 'DELETE', 'requirements' => array('path' => '.+')],
46-
['name' => 'calendar#handlePatch', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')],
47-
['name' => 'calendar#handleHead', 'url' => '/@{userId}/calendar{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')],
48-
49-
['name' => 'contacts#handleGet', 'url' => '/@{userId}/contacts{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')],
50-
['name' => 'contacts#handlePost', 'url' => '/@{userId}/contacts{path}', 'verb' => 'POST', 'requirements' => array('path' => '.+')],
51-
['name' => 'contacts#handlePut', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PUT', 'requirements' => array('path' => '.+')],
52-
['name' => 'contacts#handleDelete', 'url' => '/@{userId}/contacts{path}', 'verb' => 'DELETE', 'requirements' => array('path' => '.+')],
53-
['name' => 'contacts#handlePatch', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PATCH', 'requirements' => array('path' => '.+')],
54-
['name' => 'contacts#handleHead', 'url' => '/@{userId}/contacts{path}', 'verb' => 'HEAD', 'requirements' => array('path' => '.+')],
55-
56-
['name' => 'solidWebhook#listWebhooks', 'url' => '/webhook/list', 'verb' => 'GET'],
57-
['name' => 'solidWebhook#register', 'url' => '/webhook/register', 'verb' => 'POST'],
58-
['name' => 'solidWebhook#unregister', 'url' => '/webhook/unregister', 'verb' => 'POST'],
59-
60-
['name' => 'solidWebsocket#register', 'url' => '/websocket/register', 'verb' => 'POST'],
61-
62-
['name' => 'app#appLauncher', 'url' => '/', 'verb' => 'GET'],
63-
]
100+
'routes' => array_merge($routes, $userIdRoutes),
64101
];

solid/css/settings-admin.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#solid-admin label {
1+
#solid-admin label.narrow {
22
width: 160px;
33
vertical-align: top;
44
display: block;
@@ -8,4 +8,8 @@
88
height: 240px;
99
font-size: 12px;
1010
font-family: monospace;
11+
}
12+
#solid-admin input.textaligned {
13+
height: 1rem;
14+
min-height: unset;
1115
}

solid/js/settings-admin.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
$(document).ready(function() {
1+
$(document).ready(function () {
2+
$('#solid-enable-user-subdomains').change(function (el) {
3+
OCP.AppConfig.setValue('solid', 'userSubDomainsEnabled', this.checked ? true : false)
4+
})
5+
26
$('#solid-private-key').change(function(el) {
37
OCP.AppConfig.setValue('solid', 'privateKey', this.value);
48
});

solid/lib/AppInfo/Application.php

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,22 @@
44

55
namespace OCA\Solid\AppInfo;
66

7-
use OC\AppFramework\Utility\TimeFactory;
8-
use OC\Authentication\Events\AppPasswordCreatedEvent;
9-
use OC\Authentication\Token\IProvider;
10-
use OC\Server;
7+
use OC;
8+
use OC\AppConfig;
119

12-
use OCA\Solid\Service\UserService;
1310
use OCA\Solid\Service\SolidWebhookService;
1411
use OCA\Solid\Db\SolidWebhookMapper;
15-
use OCA\Solid\WellKnown\OpenIdConfigurationHandler;
16-
use OCA\Solid\WellKnown\SolidHandler;
1712
use OCA\Solid\Middleware\SolidCorsMiddleware;
1813

1914
use OCP\AppFramework\App;
2015
use OCP\AppFramework\Bootstrap\IBootContext;
2116
use OCP\AppFramework\Bootstrap\IBootstrap;
2217
use OCP\AppFramework\Bootstrap\IRegistrationContext;
23-
use OCP\AppFramework\IAppContainer;
24-
use OCP\Defaults;
25-
use OCP\IServerContainer;
26-
use OCP\Settings\IManager;
27-
use OCP\Util;
2818
use OCP\IDBConnection;
2919

3020
class Application extends App implements IBootstrap {
3121
public const APP_ID = 'solid';
22+
public static $userSubDomainsEnabled;
3223

3324
/**
3425
* @param array $urlParams
@@ -76,5 +67,6 @@ public function register(IRegistrationContext $context): void {
7667
}
7768

7869
public function boot(IBootContext $context): void {
70+
self::$userSubDomainsEnabled = OC::$server->get(AppConfig::class)->getValueBool(self::APP_ID, 'userSubDomainsEnabled');
7971
}
8072
}

solid/lib/BaseServerConfig.php

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
<?php
22
namespace OCA\Solid;
33

4+
use InvalidArgumentException;
45
use OCP\IConfig;
56

67
class BaseServerConfig {
7-
private IConfig $config;
88

9-
/**
10-
* @param IConfig $config
11-
*/
9+
////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\
10+
11+
public const ERROR_INVALID_ARGUMENT = 'Invalid %s for %s: %s. Must be one of %s.';
12+
13+
protected IConfig $config;
14+
15+
//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
16+
1217
public function __construct(IConfig $config) {
1318
$this->config = $config;
1419
}
@@ -91,7 +96,7 @@ public function getClients() {
9196
$clients[] = [
9297
"clientId" => $matches[1],
9398
"clientName" => $clientRegistration['client_name'],
94-
"clientBlocked" => $clientRegistration['blocked']
99+
"clientBlocked" => $clientRegistration['blocked'] ?? false,
95100
];
96101
}
97102
}
@@ -152,6 +157,7 @@ public function removeClientConfig($clientId) {
152157
unset($scopes[$clientId]);
153158
$this->config->setAppValue('solid', 'clientScopes', $scopes);
154159
}
160+
155161
public function saveClientRegistration($origin, $clientData) {
156162
$originHash = md5($origin);
157163
$existingRegistration = $this->getClientRegistration($originHash);
@@ -182,4 +188,57 @@ public function getClientRegistration($clientId) {
182188
$data = $this->config->getAppValue('solid', "client-" . $clientId, "{}");
183189
return json_decode($data, true);
184190
}
191+
192+
public function getUserSubDomainsEnabled() {
193+
$value = $this->config->getAppValue('solid', 'userSubDomainsEnabled', false);
194+
195+
return $this->castToBool($value);
196+
}
197+
198+
public function setUserSubDomainsEnabled($enabled) {
199+
$value = $this->castToBool($enabled);
200+
201+
$this->config->setAppValue('solid', 'userSubDomainsEnabled', $value);
202+
}
203+
204+
////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
205+
206+
private function castToBool(string $mixedValue): bool
207+
{
208+
$type = gettype($mixedValue);
209+
210+
if ($type === 'boolean' || $type === 'NULL' || $type === 'integer') {
211+
$value = (bool) $mixedValue;
212+
} else {
213+
if ($type === 'string') {
214+
$mixedValue = strtolower($mixedValue);
215+
if ($mixedValue === 'true' || $mixedValue === '1') {
216+
$value = true;
217+
} elseif ($mixedValue === 'false' || $mixedValue === '0' || $mixedValue === '') {
218+
$value = false;
219+
} else {
220+
$error = [
221+
'invalid' => 'value',
222+
'for' => 'userSubDomainsEnabled',
223+
'received' => $mixedValue,
224+
'expected' => implode(',', ['true', 'false', '1', '0'])
225+
];
226+
}
227+
} else {
228+
$error = [
229+
'invalid' => 'type',
230+
'for' => 'userSubDomainsEnabled',
231+
'received' => $type,
232+
'expected' => implode(',', ['boolean', 'NULL', 'integer', 'string'])
233+
];
234+
}
235+
}
236+
237+
if (isset($error)) {
238+
$errorMessage = vsprintf(self::ERROR_INVALID_ARGUMENT, $error);
239+
throw new InvalidArgumentException($errorMessage);
240+
}
241+
242+
return $value;
243+
}
185244
}

0 commit comments

Comments
 (0)