From 4d4d362ffedfbe6ef8afe8de5882292b97030a9e Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 4 Oct 2024 10:16:02 +0200 Subject: [PATCH 1/5] replace @ with ~ in urls --- solid/appinfo/routes.php | 61 ++- solid/lib/Controller/AppController.php~ | 99 ++++ solid/lib/Controller/CalendarController.php | 44 +- solid/lib/Controller/CalendarController.php~ | 278 +++++++++++ solid/lib/Controller/ContactsController.php | 44 +- solid/lib/Controller/ContactsController.php~ | 279 +++++++++++ solid/lib/Controller/GetStorageUrlTrait.php~ | 91 ++++ solid/lib/Controller/PageController.php~ | 152 ++++++ solid/lib/Controller/ProfileController.php | 47 +- solid/lib/Controller/ProfileController.php~ | 378 ++++++++++++++ solid/lib/Controller/ServerController.php~ | 449 +++++++++++++++++ .../lib/Controller/SolidWebhookController.php | 12 +- solid/lib/Controller/StorageController.php | 47 +- solid/lib/Controller/StorageController.php~ | 471 ++++++++++++++++++ 14 files changed, 2323 insertions(+), 129 deletions(-) create mode 100644 solid/lib/Controller/AppController.php~ create mode 100644 solid/lib/Controller/CalendarController.php~ create mode 100644 solid/lib/Controller/ContactsController.php~ create mode 100644 solid/lib/Controller/GetStorageUrlTrait.php~ create mode 100644 solid/lib/Controller/PageController.php~ create mode 100644 solid/lib/Controller/ProfileController.php~ create mode 100644 solid/lib/Controller/ServerController.php~ create mode 100644 solid/lib/Controller/StorageController.php~ diff --git a/solid/appinfo/routes.php b/solid/appinfo/routes.php index 2429ad9f..469902b6 100644 --- a/solid/appinfo/routes.php +++ b/solid/appinfo/routes.php @@ -15,7 +15,6 @@ * it's instantiated in there */ - $routes = [ ['name' => 'page#approval', 'url' => '/sharing/{clientId}', 'verb' => 'GET'], ['name' => 'page#handleRevoke', 'url' => '/revoke/{clientId}', 'verb' => 'DELETE'], @@ -44,33 +43,33 @@ ]; $userIdRoutes = [ - ['name' => 'page#profile', 'url' => '/@{userId}/', 'verb' => 'GET'], - - ['name' => 'profile#handleGet', 'url' => '/@{userId}/profile{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], - ['name' => 'profile#handlePut', 'url' => '/@{userId}/profile{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], - ['name' => 'profile#handlePatch', 'url' => '/@{userId}/profile{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], - ['name' => 'profile#handleHead', 'url' => '/@{userId}/profile{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], - - ['name' => 'storage#handleGet', 'url' => '/@{userId}/storage{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], - ['name' => 'storage#handlePost', 'url' => '/@{userId}/storage{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], - ['name' => 'storage#handlePut', 'url' => '/@{userId}/storage{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], - ['name' => 'storage#handleDelete', 'url' => '/@{userId}/storage{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], - ['name' => 'storage#handlePatch', 'url' => '/@{userId}/storage{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], - ['name' => 'storage#handleHead', 'url' => '/@{userId}/storage{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], - - ['name' => 'calendar#handleGet', 'url' => '/@{userId}/calendar{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], - ['name' => 'calendar#handlePost', 'url' => '/@{userId}/calendar{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], - ['name' => 'calendar#handlePut', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], - ['name' => 'calendar#handleDelete', 'url' => '/@{userId}/calendar{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], - ['name' => 'calendar#handlePatch', 'url' => '/@{userId}/calendar{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], - ['name' => 'calendar#handleHead', 'url' => '/@{userId}/calendar{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], - - ['name' => 'contacts#handleGet', 'url' => '/@{userId}/contacts{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], - ['name' => 'contacts#handlePost', 'url' => '/@{userId}/contacts{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], - ['name' => 'contacts#handlePut', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], - ['name' => 'contacts#handleDelete', 'url' => '/@{userId}/contacts{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], - ['name' => 'contacts#handlePatch', 'url' => '/@{userId}/contacts{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], - ['name' => 'contacts#handleHead', 'url' => '/@{userId}/contacts{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], + ['name' => 'page#profile', 'url' => '/~{userId}/', 'verb' => 'GET'], + + ['name' => 'profile#handleGet', 'url' => '/~{userId}/profile{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'profile#handlePut', 'url' => '/~{userId}/profile{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'profile#handlePatch', 'url' => '/~{userId}/profile{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'profile#handleHead', 'url' => '/~{userId}/profile{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], + + ['name' => 'storage#handleGet', 'url' => '/~{userId}/storage{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handlePost', 'url' => '/~{userId}/storage{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handlePut', 'url' => '/~{userId}/storage{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handleDelete', 'url' => '/~{userId}/storage{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handlePatch', 'url' => '/~{userId}/storage{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'storage#handleHead', 'url' => '/~{userId}/storage{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], + + ['name' => 'calendar#handleGet', 'url' => '/~{userId}/calendar{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handlePost', 'url' => '/~{userId}/calendar{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handlePut', 'url' => '/~{userId}/calendar{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handleDelete', 'url' => '/~{userId}/calendar{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handlePatch', 'url' => '/~{userId}/calendar{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'calendar#handleHead', 'url' => '/~{userId}/calendar{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], + + ['name' => 'contacts#handleGet', 'url' => '/~{userId}/contacts{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handlePost', 'url' => '/~{userId}/contacts{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handlePut', 'url' => '/~{userId}/contacts{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handleDelete', 'url' => '/~{userId}/contacts{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handlePatch', 'url' => '/~{userId}/contacts{path}', 'verb' => 'PATCH', 'requirements' => ['path' => '.+'], ], + ['name' => 'contacts#handleHead', 'url' => '/~{userId}/contacts{path}', 'verb' => 'HEAD', 'requirements' => ['path' => '.+'], ], ]; // @TODO: All routes NOT generated by the UrlGenerator ANYWHERE in the code need to be checked! @@ -78,13 +77,13 @@ if (Application::$userSubDomainsEnabled) { $userIdRoutes = array_map(function ($route) { if ($route['name'] === 'page#profile') { - // The profile route should be `/me` instead of `/@{userId}/` + // The profile route should be `/me` instead of `/~{userId}/` $route['url'] = '/me'; } else { // When UserSubDomains are enabled, all routes that start with - // `/@{userId}/` should just be `/`, as the userId is present + // `/~{userId}/` should just be `/`, as the userId is present // in the subdomain. - $route['url'] = preg_replace('#^/@{userId}/#', '/', $route['url']); + $route['url'] = preg_replace('#^/~{userId}/#', '/', $route['url']); } // The required userId is set to the userId from the subdomain diff --git a/solid/lib/Controller/AppController.php~ b/solid/lib/Controller/AppController.php~ new file mode 100644 index 00000000..d0b99e92 --- /dev/null +++ b/solid/lib/Controller/AppController.php~ @@ -0,0 +1,99 @@ +userId = $userId; + $this->userManager = $userManager; + $this->contactsManager = $contactsManager; + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->config = new ServerConfig($config, $urlGenerator, $userManager); + } + + private function getUserApps($userId) { + $userApps = []; + if ($this->userManager->userExists($userId)) { + $allowedClients = $this->config->getAllowedClients($userId); + foreach ($allowedClients as $clientId) { + $registration = $this->config->getClientRegistration($clientId); + $userApps[] = $registration['client_name']; + } + } + return $userApps; + } + + private function getAppsList() { + $path = __DIR__ . "/../solid-app-list.json"; + $appsListJson = file_get_contents($path); + $appsList = json_decode($appsListJson, true); + + $userApps = $this->getUserApps($this->userId); + + foreach ($appsList as $key => $app) { + $parsedOrigin = parse_url($app['launchUrl']); + $origin = $parsedOrigin['host']; + if (in_array($origin, $userApps, true)) { + $appsList[$key]['registered'] = 1; + } else { + $appsList[$key]['registered'] = 0; + } + } + return $appsList; + } + + private function getProfilePage() { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $this->userId, "path" => "/card"))) . "#me"; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function appLauncher() { + $appsList = $this->getAppsList(); + if (!$appsList) { + return new JSONResponse(array(), Http::STATUS_NOT_FOUND); + } + $appLauncherData = array( + "appsListJson" => json_encode($appsList), + "webId" => json_encode($this->getProfilePage()), + "storageUrl" => json_encode($this->getStorageUrl($this->userId)), + 'solidNavigation' => array( + "profile" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.profile", array("userId" => $this->userId))), + "launcher" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.app.appLauncher", array())), + ) + ); + $templateResponse = new TemplateResponse('solid', 'applauncher', $appLauncherData); + $policy = new ContentSecurityPolicy(); + $policy->addAllowedStyleDomain("data:"); + $policy->addAllowedScriptDomain("'self'"); + $policy->addAllowedScriptDomain("'unsafe-inline'"); + $policy->addAllowedScriptDomain("'unsafe-eval'"); + $templateResponse->setContentSecurityPolicy($policy); + return $templateResponse; + } +} diff --git a/solid/lib/Controller/CalendarController.php b/solid/lib/Controller/CalendarController.php index 046cef08..9030dabb 100644 --- a/solid/lib/Controller/CalendarController.php +++ b/solid/lib/Controller/CalendarController.php @@ -185,31 +185,31 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/calendar{path} - // and otherwise: - // index.php/apps/solid/~{userId}/calendar{path} + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/calendar{path} + // and otherwise: + // index.php/apps/solid/~{userId}/calendar{path} // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("calendar/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^calendar/", "", $path); - } - - return $this->handleRequest($userId, $path); - } + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("calendar/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^calendar/", "", $path); + } + + return $this->handleRequest($userId, $path); + } /** * @PublicPage * @NoAdminRequired diff --git a/solid/lib/Controller/CalendarController.php~ b/solid/lib/Controller/CalendarController.php~ new file mode 100644 index 00000000..439dcdb0 --- /dev/null +++ b/solid/lib/Controller/CalendarController.php~ @@ -0,0 +1,278 @@ +config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->session = $session; + + $this->setJtiStorage($connection); + } + + private function getFileSystem($userId) { + // Make sure the root folder has an acl file, as is required by the spec; + // Generate a default file granting the owner full access. + $defaultAcl = $this->generateDefaultAcl($userId); + + // Create the Nextcloud Calendar Adapter + $adapter = new \Pdsinterop\Flysystem\Adapter\NextcloudCalendar($userId, $defaultAcl); + + $graph = new \EasyRdf\Graph(); + + // Create Formats objects + $formats = new \Pdsinterop\Rdf\Formats(); + + $serverParams = $this->rawRequest->getServerParams(); + $scheme = $serverParams['REQUEST_SCHEME']; + $domain = $serverParams['SERVER_NAME']; + $path = $serverParams['REQUEST_URI']; + $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; + + // Create the RDF Adapter + $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( + $adapter, + $graph, + $formats, + $serverUri + ); + + $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); + + $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); + + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); + $filesystem->addPlugin($plugin); + + return $filesystem; + } + + private function generateDefaultAcl($userId) { + $defaultAcl = <<< EOF +# Root ACL resource for the user account +@prefix acl: . +@prefix foaf: . + +# The owner has full access to every resource in their pod. +# Other agents have no access rights, +# unless specifically authorized in other .acl resources. +<#owner> + a acl:Authorization; + acl:agent <{user-profile-uri}>; + # Set the access to the root storage folder itself + acl:accessTo <./>; + # All resources will inherit this authorization, by default + acl:default <./>; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. +EOF; + + $profileUri = $this->getUserProfile($userId); + $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); + return $defaultAcl; + } + + private function getUserProfile($userId) { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; + } + private function getCalendarUrl($userId) { + $calendarUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.calendar.handleHead", array("userId" => $userId, "path" => "foo"))); + $calendarUrl = preg_replace('/foo$/', '', $calendarUrl); + return $calendarUrl; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleRequest($userId, $path) { + $this->calendarUserId = $userId; + + $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $this->response = new \Laminas\Diactoros\Response(); + + $this->filesystem = $this->getFileSystem($userId); + + $this->resourceServer = new ResourceServer($this->filesystem, $this->response); + $this->WAC = new WAC($this->filesystem); + + $request = $this->rawRequest; + $baseUrl = $this->getCalendarUrl($userId); + $this->resourceServer->setBaseUrl($baseUrl); + $this->WAC->setBaseUrl($baseUrl); + $notifications = new SolidNotifications(); + $this->resourceServer->setNotifications($notifications); + + $dpop = $this->getDpop(); + + try { + $webId = $dpop->getWebId($request); + } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { + $response = $this->resourceServer->getResponse() + ->withStatus(Http::STATUS_CONFLICT, "Invalid token " . $e->getMessage()); + return $this->respond($response); + } + + if (!$this->WAC->isAllowed($request, $webId)) { + $response = $this->resourceServer->getResponse()->withStatus(403, "Access denied"); + return $this->respond($response); + } + + $response = $this->resourceServer->respondToRequest($request); + $response = $this->WAC->addWACHeaders($request, $response, $webId); + return $this->respond($response); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleGet($userId, $path) { + return $this->handleRequest($userId, $path); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePost($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ +<<<<<<< HEAD + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/calendar{path} + // and otherwise: + // index.php/apps/solid/~{userId}/calendar{path} + + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("calendar/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^calendar/", "", $path); + } + + return $this->handleRequest($userId, $path); + } +======= + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + + // because we got here, the request uri should look like: + // /index.php/apps/solid/~{userId}/storage{path} + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^calendar/", "", $path); + + return $this->handleRequest($userId, $path); + } +>>>>>>> 3100599 (replace @ with ~ in urls) + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleDelete($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleHead($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePatch($userId, $path) { + return $this->handleRequest($userId, $path); + } + + private function respond($response) { + $statusCode = $response->getStatusCode(); + $response->getBody()->rewind(); + $headers = $response->getHeaders(); + + $body = $response->getBody()->getContents(); + if ($statusCode > 399) { + $reason = $response->getReasonPhrase(); + $result = new JSONResponse($reason, $statusCode); + return $result; + } + + $result = new PlainResponse($body); + + foreach ($headers as $header => $values) { + foreach ($values as $value) { + $result->addHeader($header, $value); + } + } + + $result->setStatus($statusCode); + return $result; + } +} diff --git a/solid/lib/Controller/ContactsController.php b/solid/lib/Controller/ContactsController.php index 3d2f52bb..0363add2 100644 --- a/solid/lib/Controller/ContactsController.php +++ b/solid/lib/Controller/ContactsController.php @@ -186,31 +186,31 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/contacts{path} - // and otherwise: - // index.php/apps/solid/~{userId}/contacts{path} + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/contacts{path} + // and otherwise: + // index.php/apps/solid/~{userId}/contacts{path} // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("contacts/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^contacts/", "", $path); - } - - return $this->handleRequest($userId, $path); - } + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("contacts/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^contacts/", "", $path); + } + + return $this->handleRequest($userId, $path); + } /** * @PublicPage * @NoAdminRequired diff --git a/solid/lib/Controller/ContactsController.php~ b/solid/lib/Controller/ContactsController.php~ new file mode 100644 index 00000000..f346036e --- /dev/null +++ b/solid/lib/Controller/ContactsController.php~ @@ -0,0 +1,279 @@ +config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->session = $session; + + $this->setJtiStorage($connection); + } + + private function getFileSystem($userId) { + // Make sure the root folder has an acl file, as is required by the spec; + // Generate a default file granting the owner full access. + $defaultAcl = $this->generateDefaultAcl($userId); + + // Create the Nextcloud Contacts Adapter + $adapter = new \Pdsinterop\Flysystem\Adapter\NextcloudContacts($userId, $defaultAcl); + + $graph = new \EasyRdf\Graph(); + + // Create Formats objects + $formats = new \Pdsinterop\Rdf\Formats(); + + $serverParams = $this->rawRequest->getServerParams(); + $scheme = $serverParams['REQUEST_SCHEME']; + $domain = $serverParams['SERVER_NAME']; + $path = $serverParams['REQUEST_URI']; + $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; + + // Create the RDF Adapter + $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( + $adapter, + $graph, + $formats, + $serverUri + ); + + $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); + + $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); + + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); + $filesystem->addPlugin($plugin); + + return $filesystem; + } + + private function generateDefaultAcl($userId) { + $defaultAcl = <<< EOF +# Root ACL resource for the user account +@prefix acl: . +@prefix foaf: . + +# The owner has full access to every resource in their pod. +# Other agents have no access rights, +# unless specifically authorized in other .acl resources. +<#owner> + a acl:Authorization; + acl:agent <{user-profile-uri}>; + # Set the access to the root storage folder itself + acl:accessTo <./>; + # All resources will inherit this authorization, by default + acl:default <./>; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. +EOF; + + $profileUri = $this->getUserProfile($userId); + $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); + return $defaultAcl; + } + + private function getUserProfile($userId) { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; + } + private function getContactsUrl($userId) { + $contactsUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.contacts.handleHead", array("userId" => $userId, "path" => "foo"))); + $contactsUrl = preg_replace('/foo$/', '', $contactsUrl); + return $contactsUrl; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleRequest($userId, $path) { + $this->contactsUserId = $userId; + + $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $this->response = new \Laminas\Diactoros\Response(); + + $this->filesystem = $this->getFileSystem($userId); + + $this->resourceServer = new ResourceServer($this->filesystem, $this->response); + $this->WAC = new WAC($this->filesystem); + + $request = $this->rawRequest; + $baseUrl = $this->getContactsUrl($userId); + $this->resourceServer->setBaseUrl($baseUrl); + $this->WAC->setBaseUrl($baseUrl); + $notifications = new SolidNotifications(); + $this->resourceServer->setNotifications($notifications); + + $dpop = $this->getDpop(); + + try { + $webId = $dpop->getWebId($request); + } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { + $response = $this->resourceServer->getResponse() + ->withStatus(Http::STATUS_CONFLICT, "Invalid token " . $e->getMessage()); + return $this->respond($response); + } + + if (!$this->WAC->isAllowed($request, $webId)) { + $response = $this->resourceServer->getResponse()->withStatus(403, "Access denied"); + return $this->respond($response); + } + + $response = $this->resourceServer->respondToRequest($request); + $response = $this->WAC->addWACHeaders($request, $response, $webId); + return $this->respond($response); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleGet($userId, $path) { + return $this->handleRequest($userId, $path); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePost($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ +<<<<<<< HEAD + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/contacts{path} + // and otherwise: + // index.php/apps/solid/~{userId}/contacts{path} + + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("contacts/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^contacts/", "", $path); + } + + return $this->handleRequest($userId, $path); + } +======= + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + + // because we got here, the request uri should look like: + // /index.php/apps/solid/~{userId}/storage{path} + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^contacts/", "", $path); + + return $this->handleRequest($userId, $path); + } +>>>>>>> 3100599 (replace @ with ~ in urls) + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleDelete($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleHead($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePatch($userId, $path) { + return $this->handleRequest($userId, $path); + } + + private function respond($response) { + $statusCode = $response->getStatusCode(); + $response->getBody()->rewind(); + $headers = $response->getHeaders(); + + $body = $response->getBody()->getContents(); + if ($statusCode > 399) { + $reason = $response->getReasonPhrase(); + $result = new JSONResponse($reason, $statusCode); + return $result; + } + + $result = new PlainResponse($body); + + foreach ($headers as $header => $values) { + foreach ($values as $value) { + $result->addHeader($header, $value); + } + } + + $result->setStatus($statusCode); + return $result; + } +} diff --git a/solid/lib/Controller/GetStorageUrlTrait.php~ b/solid/lib/Controller/GetStorageUrlTrait.php~ new file mode 100644 index 00000000..97a312b7 --- /dev/null +++ b/solid/lib/Controller/GetStorageUrlTrait.php~ @@ -0,0 +1,91 @@ +config = $config; + } + + final public function setUrlGenerator(IURLGenerator $urlGenerator): void + { + $this->urlGenerator = $urlGenerator; + } + + ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + protected ServerConfig $config; + protected IURLGenerator $urlGenerator; + + /////////////////////////////// PROTECTED API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + /** + * @FIXME: Add check for bob.nextcloud.local/solid/alice to throw 404 + * @TODO: Use route without `@alice` in /apps/solid/@alice/profile/card#me when user-domains are enabled + */ + public function getStorageUrl($userId) { + $routeUrl = $this->urlGenerator->linkToRoute( + 'solid.storage.handleHead', + ['userId' => $userId, 'path' => 'foo'] + ); + + $storageUrl = $this->urlGenerator->getAbsoluteURL($routeUrl); + + // (?) $storageUrl = preg_replace('/foo$/', '', $storageUrl); + $storageUrl = preg_replace('/foo$/', '/', $storageUrl); + + if ($this->config->getUserSubDomainsEnabled()) { + $url = parse_url($storageUrl); + + if (strpos($url['host'], $userId . '.') !== false) { + $url['host'] = str_replace($userId . '.', '', $url['host']); + } + + $url['host'] = $userId . '.' . $url['host']; // $storageUrl = $userId . '.' . $storageUrl; + $storageUrl = $this->build_url($url); + } + + return $storageUrl; + } + + public function validateUrl(RequestInterface $request): bool { + $isValid = false; + + $host = $request->getUri()->getHost(); + $path = $request->getUri()->getPath(); + $pathParts = explode('/', $path); + + $pathUsers = array_filter($pathParts, static function ($value) { + return str_starts_with($value, '@'); + }); + + if (count($pathUsers) === 1) { + $pathUser = reset($pathUsers); + $subDomainUser = explode('.', $host)[0]; + + $isValid = $pathUser === '@' . $subDomainUser; + } + + return $isValid; + } + + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + private function build_url(array $parts) { + // @FIXME: Replace with existing more robust URL builder + return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') . + (isset($parts['host']) ? "//{$parts['host']}" : '') . + (isset($parts['port']) ? ":{$parts['port']}" : '') . + (isset($parts['path']) ? "{$parts['path']}" : '') . + (isset($parts['query']) ? "?{$parts['query']}" : '') . + (isset($parts['fragment']) ? "#{$parts['fragment']}" : ''); + } +} diff --git a/solid/lib/Controller/PageController.php~ b/solid/lib/Controller/PageController.php~ new file mode 100644 index 00000000..3109ad8e --- /dev/null +++ b/solid/lib/Controller/PageController.php~ @@ -0,0 +1,152 @@ +userId = $userId; + $this->userManager = $userManager; + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); + } + + /** + * CAUTION: the @Stuff turns off security checks; for this page no admin is + * required and no CSRF check. If you don't know what CSRF is, read + * it up in the docs or you might create a security hole. This is + * basically the only required method to add this exemption, don't + * add it to any other method if you don't exactly know what it does + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function index() { + return new TemplateResponse('solid', 'index'); // templates/index.php + } + + private function getUserProfile($userId) { + if ($this->userManager->userExists($userId)) { + $user = $this->userManager->get($userId); + if ($user !== null) { + $profile = array( + 'id' => $userId, + 'displayName' => $user->getDisplayName(), + 'profileUri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me", + 'solidNavigation' => array( + "profile" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.profile", array("userId" => $userId))), + "launcher" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.app.appLauncher", array())), + ) + ); + return $profile; + } + } + return false; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function profile($userId) { + // header("Access-Control-Allow-Headers: *, authorization, accept, content-type"); + // header("Access-Control-Allow-Credentials: true"); + $profile = $this->getUserProfile($userId); + if (!$profile) { + return new JSONResponse(array(), Http::STATUS_NOT_FOUND); + } + $templateResponse = new TemplateResponse('solid', 'profile', $profile); + $policy = new ContentSecurityPolicy(); + $policy->addAllowedStyleDomain("data:"); + $templateResponse->setContentSecurityPolicy($policy); + return $templateResponse; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function approval($clientId) { + $clientRegistration = $this->config->getClientRegistration($clientId); + $params = array( + "clientId" => $clientId, + "clientName" => $clientRegistration['client_name'], + "serverName" => "Nextcloud", + "returnUrl" => $_GET['returnUrl'], + ); + $templateResponse = new TemplateResponse('solid', 'sharing', $params); + + $policy = new ContentSecurityPolicy(); + $policy->addAllowedStyleDomain("data:"); + + $parsedOrigin = parse_url($clientRegistration['redirect_uris'][0]); + $origin = $parsedOrigin['host']; + if ($origin) { + $policy->addAllowedFormActionDomain($parsedOrigin['scheme'] . "://" . $origin); + $templateResponse->setContentSecurityPolicy($policy); + } + return $templateResponse; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function customscheme() { + $templateResponse = new TemplateResponse('solid', 'customscheme'); + return $templateResponse; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleApproval($clientId) { + $approval = $_POST['approval']; + if ($approval == "allow") { + $this->config->addAllowedClient($this->userId, $clientId); + } else { + $this->config->removeAllowedClient($this->userId, $clientId); + } + $authUrl = $_POST['returnUrl']; + + $result = new JSONResponse("ok"); + + $result->setStatus("302"); + $result->addHeader("Location", $authUrl); + return $result; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleRevoke($clientId) { + $this->config->removeAllowedClient($this->userId, $clientId); + $result = new JSONResponse("ok"); + return $result; + } +} diff --git a/solid/lib/Controller/ProfileController.php b/solid/lib/Controller/ProfileController.php index 24560dc7..66391438 100644 --- a/solid/lib/Controller/ProfileController.php +++ b/solid/lib/Controller/ProfileController.php @@ -204,31 +204,30 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/profile{path} - // and otherwise: - // index.php/apps/solid/~{userId}/profile{path} - + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/profile{path} + // and otherwise: + // index.php/apps/solid/~{userId}/profile{path} // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("profile/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^profile/", "", $path); - } - - return $this->handleRequest($userId, $path); - } + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("profile/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^profile/", "", $path); + } + + return $this->handleRequest($userId, $path); + } /** * @PublicPage * @NoAdminRequired diff --git a/solid/lib/Controller/ProfileController.php~ b/solid/lib/Controller/ProfileController.php~ new file mode 100644 index 00000000..70ba5e12 --- /dev/null +++ b/solid/lib/Controller/ProfileController.php~ @@ -0,0 +1,378 @@ +config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->userManager = $userManager; + $this->contactsManager = $contactsManager; + $this->session = $session; + + $this->setJtiStorage($connection); + } + + private function getFileSystem($userId) { + // Make sure the root folder has an acl file, as is required by the spec; + // Generate a default file granting the owner full access. + $defaultAcl = $this->generateDefaultAcl($userId); + $profile = $this->generateTurtleProfile($userId); + + // Create the Nextcloud Calendar Adapter + $adapter = new \Pdsinterop\Flysystem\Adapter\NextcloudProfile($userId, $profile, $defaultAcl, $this->config); + + $graph = new \EasyRdf\Graph(); + // Create Formats objects + $formats = new \Pdsinterop\Rdf\Formats(); + + $serverParams = $this->rawRequest->getServerParams(); + $scheme = $serverParams['REQUEST_SCHEME']; + $domain = $serverParams['SERVER_NAME']; + $path = $serverParams['REQUEST_URI']; + $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; + + // Create the RDF Adapter + $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( + $adapter, + $graph, + $formats, + $serverUri + ); + + $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); + + $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); + + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); + $filesystem->addPlugin($plugin); + + return $filesystem; + } + + private function generateDefaultAcl($userId) { + $defaultAcl = <<< EOF +# Root ACL resource for the user account +@prefix acl: . +@prefix foaf: . + +# The profile is readable by the public +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo <./>; + acl:default <./>; + acl:mode acl:Read. + +# The owner has full access to every resource in their pod. +# Other agents have no access rights, +# unless specifically authorized in other .acl resources. +<#owner> + a acl:Authorization; + acl:agent <{user-profile-uri}>; + # Set the access to the root storage folder itself + acl:accessTo <./>; + # All resources will inherit this authorization, by default + acl:default <./>; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. +EOF; + + $profileUri = $this->getUserProfileUri($userId); + $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); + return $defaultAcl; + } + + private function getUserProfileUri($userId) { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; + } + private function getProfileUrl($userId) { + $profileUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleHead", array("userId" => $userId, "path" => "foo"))); + $profileUrl = preg_replace('/foo$/', '', $profileUrl); + return $profileUrl; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleRequest($userId, $path) { + $this->userId = $userId; + + $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $this->response = new \Laminas\Diactoros\Response(); + + $this->filesystem = $this->getFileSystem($userId); + + $this->resourceServer = new ResourceServer($this->filesystem, $this->response); + $this->WAC = new WAC($this->filesystem); + + $request = $this->rawRequest; + $baseUrl = $this->getProfileUrl($userId); + $this->resourceServer->setBaseUrl($baseUrl); + $this->WAC->setBaseUrl($baseUrl); + $notifications = new SolidNotifications(); + $this->resourceServer->setNotifications($notifications); + + $dpop = $this->getDpop(); + + if ($request->getHeaderLine("DPop")) { + try { + $webId = $dpop->getWebId($request); + } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { + $response = $this->resourceServer->getResponse() + ->withStatus(Http::STATUS_CONFLICT, "Invalid token " . $e->getMessage()); + return $this->respond($response); + } + } else { + $webId = ""; + } + + if (!$this->WAC->isAllowed($request, $webId)) { + $response = $this->resourceServer->getResponse()->withStatus(403, "Access denied"); + return $this->respond($response); + } + + $response = $this->resourceServer->respondToRequest($request); + $response = $this->WAC->addWACHeaders($request, $response, $webId); + return $this->respond($response); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleGet($userId, $path) { + //TODO: check that the $userId matches the userDomain, if enabled. + return $this->handleRequest($userId, $path); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePost($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + +<<<<<<< HEAD + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/profile{path} + // and otherwise: + // index.php/apps/solid/~{userId}/profile{path} +======= + // because we got here, the request uri should look like: + // /index.php/apps/solid/~{userId}/storage{path} + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^profile/", "", $path); +>>>>>>> 3100599 (replace @ with ~ in urls) + + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("profile/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^profile/", "", $path); + } + + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleDelete($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleHead($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePatch($userId, $path) { + return $this->handleRequest($userId, $path); + } + + private function respond($response) { + $statusCode = $response->getStatusCode(); + $response->getBody()->rewind(); + $headers = $response->getHeaders(); + + $body = $response->getBody()->getContents(); + if ($statusCode > 399) { + $reason = $response->getReasonPhrase(); + $result = new JSONResponse($reason, $statusCode); + return $result; + } + + $result = new PlainResponse($body); + + foreach ($headers as $header => $values) { + foreach ($values as $value) { + $result->addHeader($header, $value); + } + } +// $origin = $_SERVER['HTTP_ORIGIN'] ?? "*"; +// $result->addHeader('Access-Control-Allow-Credentials', 'true'); +// $result->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); +// $result->addHeader('Access-Control-Allow-Origin', $origin); + $result->setStatus($statusCode); + return $result; + } + + private function getUserProfile($userId) { + if ($this->userManager->userExists($userId)) { + $user = $this->userManager->get($userId); + $addressBooks = $this->contactsManager->getUserAddressBooks(); + $friends = []; + foreach($addressBooks as $k => $v) { + $results = $addressBooks[$k]->search('', ['FN'], ['types' => true]); + foreach($results as $found) { + if (isset($found['URL']) && is_array($found['URL'])) { + foreach($found['URL'] as $i => $obj) { + array_push($friends, $obj['value']); + } + } + } + } + //TODO: privateTypeIndex and publisTypeIndex need to user getStorageURL + if ($user !== null) { + $profile = array( + 'id' => $userId, + 'displayName' => $user->getDisplayName(), + 'profileUri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me", + 'friends' => $friends, + 'inbox' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/inbox/"))), + 'preferences' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/preferences.ttl"))), + 'privateTypeIndex' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/privateTypeIndex.ttl"))), + 'publicTypeIndex' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/publicTypeIndex.ttl"))), + 'storage' => $this->getStorageUrl($userId), + 'issuer' => $this->urlGenerator->getBaseURL() + ); + return $profile; + } + } + return false; + } + + private function generateTurtleProfile($userId) { + $profile = $this->getUserProfile($userId); + if (!$profile) { + return ""; + } + ob_start(); + ?>@prefix : <#>. + @prefix solid: . + @prefix pro: <./>. + @prefix foaf: . + @prefix schem: . + @prefix acl: . + @prefix ldp: . + @prefix inbox: <>. + @prefix sp: . + @prefix ser: <>. + + pro:card a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me. + + :me + a schem:Person, foaf:Person; + ldp:inbox inbox:; + sp:preferencesFile <>; + sp:storage ser:; + solid:account ser:; + solid:privateTypeIndex <>; + solid:publicTypeIndex <>; + solid:oidcIssuer <>; + $friend) { + ?> + foaf:knows <>; + + foaf:name ""; + "". + urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))); + $baseProfile = $this->config->getProfileData($userId); + $graph = new \EasyRdf\Graph(); + $graph->parse($baseProfile, "turtle", $baseUrl); + $graph->parse($generatedProfile, "turtle", $baseUrl); + $combinedProfile = $graph->serialise("turtle"); + return $combinedProfile; + } +} diff --git a/solid/lib/Controller/ServerController.php~ b/solid/lib/Controller/ServerController.php~ new file mode 100644 index 00000000..9c9044ee --- /dev/null +++ b/solid/lib/Controller/ServerController.php~ @@ -0,0 +1,449 @@ +config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); + $this->userId = $userId; + $this->userManager = $userManager; + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->session = $session; + + $this->setJtiStorage($connection); + + $this->authServerConfig = $this->createAuthServerConfig(); + $this->authServerFactory = (new \Pdsinterop\Solid\Auth\Factory\AuthorizationServerFactory($this->authServerConfig))->create(); + + $this->tokenGenerator = new \Pdsinterop\Solid\Auth\TokenGenerator( + $this->authServerConfig, + $this->getDpopValidFor(), + $this->getDpop() + ); + } + + private function getOpenIdEndpoints() { + return [ + 'issuer' => $this->urlGenerator->getBaseURL(), + 'authorization_endpoint' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.authorize")), + 'jwks_uri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.jwks")), + "check_session_iframe" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.session")), + "end_session_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.logout")), + "token_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.token")), + "userinfo_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.userinfo")), + "registration_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.register")) + ]; + } + + private function getKeys() { + $encryptionKey = $this->config->getEncryptionKey(); + $privateKey = $this->config->getPrivateKey(); + $key = openssl_pkey_get_private($privateKey); + $publicKey = openssl_pkey_get_details($key)['key']; + return [ + "encryptionKey" => $encryptionKey, + "privateKey" => $privateKey, + "publicKey" => $publicKey + ]; + } + + private function createAuthServerConfig() { + $clientId = isset($_GET['client_id']) ? $_GET['client_id'] : null; + $client = $this->getClient($clientId); + $keys = $this->getKeys(); + try { + return (new \Pdsinterop\Solid\Auth\Factory\ConfigFactory( + $client, + $keys['encryptionKey'], + $keys['privateKey'], + $keys['publicKey'], + $this->getOpenIdEndpoints() + ))->create(); + } catch(\Throwable $e) { + // var_dump($e); + return null; + } + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function cors($path) { + $origin = $_SERVER['HTTP_ORIGIN']; + return (new DataResponse('OK')); +// ->addHeader('Access-Control-Allow-Origin', $origin) +// ->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization') +// ->addHeader('Access-Control-Allow-Methods', 'POST') +// ->addHeader('Access-Control-Allow-Credentials', 'true'); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function authorize() { + // Create a request + if (!$this->userManager->userExists($this->userId)) { + $result = new JSONResponse('Authorization required'); + $result->setStatus(401); + return $result; +// return $result->addHeader('Access-Control-Allow-Origin', '*'); + } + + if (isset($_GET['request'])) { + $jwtConfig = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($this->config->getPrivateKey())); + try { + $token = $jwtConfig->parser()->parse($_GET['request']); + $this->session->set("nonce", $token->claims()->get('nonce')); + } catch(\Exception $e) { + $this->session->set("nonce", $_GET['nonce']); + } + } + + $getVars = $_GET; + if (!isset($getVars['grant_type'])) { + $getVars['grant_type'] = 'implicit'; + } + $getVars['response_type'] = $this->getResponseType(); + $getVars['scope'] = "openid" ; + + if (!isset($getVars['redirect_uri'])) { + if (!isset($token)) { + $result = new JSONResponse('Bad request, does not contain valid token'); + $result->setStatus(400); + return $result; +// return $result->addHeader('Access-Control-Allow-Origin', '*'); + } + try { + $getVars['redirect_uri'] = $token->claims()->get("redirect_uri"); + } catch(\Exception $e) { + $result = new JSONResponse('Bad request, missing redirect uri'); + $result->setStatus(400); + return $result; +// return $result->addHeader('Access-Control-Allow-Origin', '*'); + } + } + + if (preg_match("/^http(s)?:/", $getVars['client_id'])) { + $parsedOrigin = parse_url($getVars['redirect_uri']); + $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; + if (isset($parsedOrigin['port'])) { + $origin .= ":" . $parsedOrigin['port']; + } + $clientData = array( + "client_id_issued_at" => time(), + "client_name" => $getVars['client_id'], + "origin" => $origin, + "redirect_uris" => array( + $getVars['redirect_uri'] + ) + ); + $clientId = $this->config->saveClientRegistration($origin, $clientData)['client_id']; + $clientId = $this->config->saveClientRegistration($getVars['client_id'], $clientData)['client_id']; + $returnUrl = $getVars['redirect_uri']; + } else { + $clientId = $getVars['client_id']; + $returnUrl = $_SERVER['REQUEST_URI']; + } + + $clientRegistration = $this->config->getClientRegistration($clientId); + if (isset($clientRegistration['blocked']) && ($clientRegistration['blocked'] === true)) { + $result = new JSONResponse('Unauthorized client'); + $result->setStatus(403); + return $result; + } + + $approval = $this->checkApproval($clientId); + if (!$approval) { + $result = new JSONResponse('Approval required'); + $result->setStatus(302); + $approvalUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.approval", array("clientId" => $clientId, "returnUrl" => $returnUrl))); + $result->addHeader("Location", $approvalUrl); + return $result; // ->addHeader('Access-Control-Allow-Origin', '*'); + } + + $parsedOrigin = parse_url($clientRegistration['redirect_uris'][0]); + if ($parsedOrigin['scheme'] != "https" && !isset($_GET['customscheme'])) { + $result = new JSONResponse('Custom schema'); + $result->setStatus(302); + $originalRequest = parse_url($_SERVER['REQUEST_URI']); + $customSchemeUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.customscheme")) . ($originalRequest['query'] ? "?" . $originalRequest['query'] . "&customscheme=" . $parsedOrigin['scheme'] : ''); + $result->addHeader("Location", $customSchemeUrl); + return $result; + } + + $user = new \Pdsinterop\Solid\Auth\Entity\User(); + $user->setIdentifier($this->getProfilePage()); + + $request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $getVars, $_POST, $_COOKIE, $_FILES); + $response = new \Laminas\Diactoros\Response(); + $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); + + $response = $server->respondToAuthorizationRequest($request, $user, $approval); + $response = $this->tokenGenerator->addIdTokenToResponse( + $response, + $clientId, + $this->getProfilePage(), + $this->session->get("nonce"), + $this->config->getPrivateKey() + ); + + return $this->respond($response); // ->addHeader('Access-Control-Allow-Origin', '*'); + } + + private function checkApproval($clientId) { + $allowedClients = $this->config->getAllowedClients($this->userId); + if ($clientId == md5("tester")) { // FIXME: Double check that this is not a security issue; It is only here to help the test suite; + return \Pdsinterop\Solid\Auth\Enum\Authorization::APPROVED; + } + if ($clientId == md5("https://tester")) { // FIXME: Double check that this is not a security issue; It is only here to help the test suite; + return \Pdsinterop\Solid\Auth\Enum\Authorization::APPROVED; + } + if (in_array($clientId, $allowedClients)) { + return \Pdsinterop\Solid\Auth\Enum\Authorization::APPROVED; + } else { + return \Pdsinterop\Solid\Auth\Enum\Authorization::DENIED; + } + } + + private function getProfilePage() { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $this->userId, "path" => "/card"))) . "#me"; + } + + private function getResponseType() { + $responseTypes = explode(" ", $_GET['response_type']); + foreach ($responseTypes as $responseType) { + switch ($responseType) { + case "token": + return "token"; + break; + case "code": + return "code"; + break; + } + } + return "token"; // default to token response type; + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function session() { + return new JSONResponse("ok"); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function token() { + $request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $code = $request->getParsedBody()['code']; + $clientId = $request->getParsedBody()['client_id']; + + $httpDpop = $request->getServerParams()['HTTP_DPOP']; + + $response = new \Laminas\Diactoros\Response(); + $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); + $response = $server->respondToAccessTokenRequest($request); + + // FIXME: not sure if decoding this here is the way to go. + // FIXME: because this is a public page, the nonce from the session is not available here. + $codeInfo = $this->tokenGenerator->getCodeInfo($code); + $response = $this->tokenGenerator->addIdTokenToResponse( + $response, + $clientId, + $codeInfo['user_id'], + ($_SESSION['nonce'] ?? ''), + $this->config->getPrivateKey(), + $httpDpop + ); + + return $this->respond($response); // ->addHeader('Access-Control-Allow-Origin', '*'); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function userinfo() { + return new JSONResponse("ok"); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function logout() { + $this->userService->logout(); + return new JSONResponse("ok"); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function register() { + $clientData = file_get_contents('php://input'); + $clientData = json_decode($clientData, true); + if (!$clientData['redirect_uris']) { + return new JSONResponse("Missing redirect URIs"); + } + $clientData['client_id_issued_at'] = time(); + $parsedOrigin = parse_url($clientData['redirect_uris'][0]); + $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; + if (isset($parsedOrigin['port'])) { + $origin .= ":" . $parsedOrigin['port']; + } + + $clientData = $this->config->saveClientRegistration($origin, $clientData); + $registration = array( + 'client_id' => $clientData['client_id'], + /* + FIXME: returning client_secret will trigger calls with basic auth to us. To get this to work, we need this patch: + // File /var/www/vhosts/solid-nextcloud/site/www/lib/base.php not changed so no update needed + // ($request->getRawPathInfo() !== '/apps/oauth2/api/v1/token') && + // ($request->getRawPathInfo() !== '/apps/solid/token') + */ + // 'client_secret' => $clientData['client_secret'], // FIXME: Returning this means we need to patch Nextcloud to accept tokens on calls to + + 'registration_client_uri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.registeredClient", array("clientId" => $clientData['client_id']))), + 'client_id_issued_at' => $clientData['client_id_issued_at'], + 'redirect_uris' => $clientData['redirect_uris'], + ); + $registration = $this->tokenGenerator->respondToRegistration($registration, $this->config->getPrivateKey()); + return (new JSONResponse($registration)); +// ->addHeader('Access-Control-Allow-Origin', $origin) +// ->addHeader('Access-Control-Allow-Methods', 'POST'); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function registeredClient($clientId) { + $clientRegistration = $this->config->getClientRegistration($clientId); + unset($clientRegistration['client_secret']); + return new JSONResponse($clientRegistration); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function jwks() { + $response = new \Laminas\Diactoros\Response(); + $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); + $response = $server->respondToJwksMetadataRequest(); + return $this->respond($response); + } + + private function respond($response) { + $statusCode = $response->getStatusCode(); + $response->getBody()->rewind(); + $headers = $response->getHeaders(); + + $body = json_decode($response->getBody()->getContents()); + if ($statusCode > 399) { + // var_dump($body); + $reason = $response->getReasonPhrase(); + $result = new JSONResponse($reason, $statusCode); + return $result; + } + + if ($body == null) { + $body = 'ok'; + } + $result = new JSONResponse($body); + + foreach ($headers as $header => $values) { + foreach ($values as $value) { + $result->addHeader($header, $value); + } + } + $result->setStatus($statusCode); +// $result->addHeader('Access-Control-Allow-Origin', '*'); + return $result; + } + + private function getClient($clientId) { + $clientRegistration = $this->config->getClientRegistration($clientId); + + if ($clientId && count($clientRegistration)) { + return new \Pdsinterop\Solid\Auth\Config\Client( + $clientId, + $clientRegistration['client_secret'] ?? '', + $clientRegistration['redirect_uris'], + $clientRegistration['client_name'] + ); + } else { + return new \Pdsinterop\Solid\Auth\Config\Client('','',array(),''); + } + } +} diff --git a/solid/lib/Controller/SolidWebhookController.php b/solid/lib/Controller/SolidWebhookController.php index 371e3c02..5846097d 100644 --- a/solid/lib/Controller/SolidWebhookController.php +++ b/solid/lib/Controller/SolidWebhookController.php @@ -150,13 +150,13 @@ private function initializeStorage($userId) { } private function parseTopic($topic) { - // topic = https://nextcloud.server/solid/@alice/storage/foo/bar + // topic = https://nextcloud.server/solid/~alice/storage/foo/bar $appBaseUrl = $this->getAppBaseUrl(); // https://nextcloud.server/solid/ - $internalUrl = str_replace($appBaseUrl, '', $topic); // @alice/storage/foo/bar + $internalUrl = str_replace($appBaseUrl, '', $topic); // ~alice/storage/foo/bar $pathicles = explode("/", $internalUrl); - $userId = $pathicles[0]; // @alice - $userId = preg_replace("/^@/", "", $userId); // alice - $storageUrl = $this->getStorageUrl($userId); // https://nextcloud.server/solid/@alice/storage/ + $userId = $pathicles[0]; // ~alice + $userId = preg_replace("/^~/", "", $userId); // alice + $storageUrl = $this->getStorageUrl($userId); // https://nextcloud.server/solid/~alice/storage/ $storagePath = str_replace($storageUrl, '/', $topic); // /foo/bar return array( "userId" => $userId, @@ -182,7 +182,7 @@ private function createGetRequest($topic) { } private function checkReadAccess($topic) { - // split out $topic into $userId and $path https://nextcloud.server/solid/@alice/storage/foo/bar + // split out $topic into $userId and $path https://nextcloud.server/solid/~alice/storage/foo/bar // - userId in this case is the pod owner (not the one doing the request). (alice) // - path is the path within the storage pod (/foo/bar) $target = $this->parseTopic($topic); diff --git a/solid/lib/Controller/StorageController.php b/solid/lib/Controller/StorageController.php index de4cd201..4a834d3b 100644 --- a/solid/lib/Controller/StorageController.php +++ b/solid/lib/Controller/StorageController.php @@ -375,31 +375,30 @@ public function handlePost($userId, $path) { * @NoAdminRequired * @NoCSRFRequired */ - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/storage{path} - // and otherwise: - // index.php/apps/solid/~{userId}/storage{path} - + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/storage{path} + // and otherwise: + // index.php/apps/solid/~{userId}/storage{path} // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("storage/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^storage/", "", $path); - } - - return $this->handleRequest($userId, $path); - } + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("storage/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^storage/", "", $path); + } + + return $this->handleRequest($userId, $path); + } /** * @PublicPage * @NoAdminRequired diff --git a/solid/lib/Controller/StorageController.php~ b/solid/lib/Controller/StorageController.php~ new file mode 100644 index 00000000..07541d13 --- /dev/null +++ b/solid/lib/Controller/StorageController.php~ @@ -0,0 +1,471 @@ +config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); + $this->rootFolder = $rootFolder; + $this->request = $request; + $this->urlGenerator = $urlGenerator; + $this->session = $session; + + $this->setJtiStorage($connection); + } + + private function getFileSystem() { + // Create the Nextcloud Adapter + $adapter = new \Pdsinterop\Flysystem\Adapter\Nextcloud($this->solidFolder); + $graph = new \EasyRdf\Graph(); + + // Create Formats objects + $formats = new \Pdsinterop\Rdf\Formats(); + + $serverParams = $this->rawRequest->getServerParams(); + $scheme = $serverParams['REQUEST_SCHEME']; + $domain = $serverParams['SERVER_NAME']; + $path = $serverParams['REQUEST_URI']; + $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; + + // Create the RDF Adapter + $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( + $adapter, + $graph, + $formats, + $serverUri + ); + + $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); + + $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); + + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); + $filesystem->addPlugin($plugin); + + return $filesystem; + } + + private function getUserProfile($userId) { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; + } + + private function generateDefaultAcl($userId) { + $defaultAcl = <<< EOF +# Root ACL resource for the user account +@prefix acl: . +@prefix foaf: . + +# The homepage is readable by the public +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo <./>; + acl:mode acl:Read. + +# The owner has full access to every resource in their pod. +# Other agents have no access rights, +# unless specifically authorized in other .acl resources. +<#owner> + a acl:Authorization; + acl:agent <{user-profile-uri}>; + # Set the access to the root storage folder itself + acl:accessTo <./>; + # All resources will inherit this authorization, by default + acl:default <./>; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. +EOF; + + $profileUri = $this->getUserProfile($userId); + $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); + return $defaultAcl; + } + + private function generatePublicAppendAcl($userId) { + $publicAppendAcl = <<< EOF +# Inbox ACL resource for the user account +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo <./>; + acl:default <./>; + acl:mode + acl:Append. + +<#owner> + a acl:Authorization; + acl:agent <{user-profile-uri}>; + # Set the access to the root storage folder itself + acl:accessTo <./>; + # All resources will inherit this authorization, by default + acl:default <./>; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. +EOF; + + $profileUri = $this->getUserProfile($userId); + $publicAppendAcl = str_replace("{user-profile-uri}", $profileUri, $publicAppendAcl); + return $publicAppendAcl; + } + + private function generatePublicReadAcl($userId) { + $publicReadAcl = <<< EOF +# Inbox ACL resource for the user account +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo <./>; + acl:default <./>; + acl:mode + acl:Read. + +<#owner> + a acl:Authorization; + acl:agent <{user-profile-uri}>; + # Set the access to the root storage folder itself + acl:accessTo <./>; + # All resources will inherit this authorization, by default + acl:default <./>; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. +EOF; + + $profileUri = $this->getUserProfile($userId); + $publicReadAcl = str_replace("{user-profile-uri}", $profileUri, $publicReadAcl); + return $publicReadAcl; + } + + private function generateDefaultPublicTypeIndex() { + $publicTypeIndex = <<< EOF +# Public type index +@prefix : <#>. +@prefix solid: . + +<> + a solid:ListedDocument, solid:TypeIndex. +EOF; + + return $publicTypeIndex; + } + + private function generateDefaultPrivateTypeIndex() { + $privateTypeIndex = <<< EOF +# Private type index +@prefix : <#>. +@prefix solid: . + +<> + a solid:UnlistedDocument, solid:TypeIndex. +EOF; + + return $privateTypeIndex; + } + private function generateDefaultPreferences($userId) { + $preferences = <<< EOF +# Preferences +@prefix : <#>. +@prefix sp: . +@prefix dct: . +@prefix profile: <{user-profile-uri}>. +@prefix solid: . + +<> + a sp:ConfigurationFile; + dct:title "Preferences file". + +profile:me + a solid:Developer; + solid:privateTypeIndex ; + solid:publicTypeIndex . +EOF; + + $profileUri = $this->getUserProfile($userId); + $preferences = str_replace("{user-profile-uri}", $profileUri, $preferences); + return $preferences; + } + private function initializeStorage($userId) { + $this->userFolder = $this->rootFolder->getUserFolder($userId); + if (!$this->userFolder->nodeExists("solid")) { + $this->userFolder->newFolder("solid"); // Create the Solid directory for storage if it doesn't exist. + } + $this->solidFolder = $this->userFolder->get("solid"); + + $this->filesystem = $this->getFileSystem(); + + // Make sure the root folder has an acl file, as is required by the spec; + // Generate a default file granting the owner full access if there is nothing there. + if (!$this->filesystem->has("/.acl")) { + $defaultAcl = $this->generateDefaultAcl($userId); + $this->filesystem->write("/.acl", $defaultAcl); + } + + // Generate default folders and ACLs: + if (!$this->filesystem->has("/inbox")) { + $this->filesystem->createDir("/inbox"); + } + if (!$this->filesystem->has("/inbox/.acl")) { + $inboxAcl = $this->generatePublicAppendAcl($userId); + $this->filesystem->write("/inbox/.acl", $inboxAcl); + } + if (!$this->filesystem->has("/settings")) { + $this->filesystem->createDir("/settings"); + } + if (!$this->filesystem->has("/settings/privateTypeIndex.ttl")) { + $privateTypeIndex = $this->generateDefaultPrivateTypeIndex(); + $this->filesystem->write("/settings/privateTypeIndex.ttl", $privateTypeIndex); + } + if (!$this->filesystem->has("/settings/publicTypeIndex.ttl")) { + $publicTypeIndex = $this->generateDefaultPublicTypeIndex(); + $this->filesystem->write("/settings/publicTypeIndex.ttl", $publicTypeIndex); + } + if (!$this->filesystem->has("/settings/preferences.ttl")) { + $preferences = $this->generateDefaultPreferences($userId); + $this->filesystem->write("/settings/preferences.ttl", $preferences); + } + if (!$this->filesystem->has("/public")) { + $this->filesystem->createDir("/public"); + } + if (!$this->filesystem->has("/public/.acl")) { + $publicAcl = $this->generatePublicReadAcl($userId); + $this->filesystem->write("/public/.acl", $publicAcl); + } + if (!$this->filesystem->has("/private")) { + $this->filesystem->createDir("/private"); + } + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleRequest($userId, $path) { + $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); + $this->response = new \Laminas\Diactoros\Response(); + + $this->initializeStorage($userId); + + $this->resourceServer = new ResourceServer($this->filesystem, $this->response); + $this->WAC = new WAC($this->filesystem); + + $request = $this->rawRequest; + $baseUrl = $this->getStorageUrl($userId); + $this->resourceServer->setBaseUrl($baseUrl); + $this->WAC->setBaseUrl($baseUrl); + + $notifications = new SolidNotifications(); + $this->resourceServer->setNotifications($notifications); + + $dpop = $this->getDpop(); + + $error = false; + try { + $webId = $dpop->getWebId($request); + } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { + $error = $e; + } + + if (!isset($webId)) { + $bearer = $this->getBearer(); + try { + $webId = $bearer->getWebId($request); + } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { + $error = $e; + } + } + + if (!isset($webId)) { + $response = $this->resourceServer->getResponse() + ->withStatus(Http::STATUS_CONFLICT, "Invalid token"); + return $this->respond($response); + } + + $origin = $request->getHeaderLine("Origin"); + $allowedClients = $this->config->getAllowedClients($userId); + $allowedOrigins = array(); + foreach ($allowedClients as $clientId) { + $clientRegistration = $this->config->getClientRegistration($clientId); + if (isset($clientRegistration['client_name'])) { + $allowedOrigins[] = $clientRegistration['client_name']; + } + if (isset($clientRegistration['origin'])) { + $allowedOrigins[] = $clientRegistration['origin']; + } + } + if (!$this->WAC->isAllowed($request, $webId, $origin, $allowedOrigins)) { + $response = $this->resourceServer->getResponse() + ->withStatus(403, "Access denied"); + return $this->respond($response); + } + $response = $this->resourceServer->respondToRequest($request); + $response = $this->WAC->addWACHeaders($request, $response, $webId); + return $this->respond($response); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleGet($userId, $path) { + return $this->handleRequest($userId, $path); + } + + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePost($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePut() { // $userId, $path) { + // FIXME: Adding the correct variables in the function name will make nextcloud + // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; + +<<<<<<< HEAD + // because we got here, the request uri should look like: + // - if we have user subdomains enabled: + // /index.php/apps/solid/storage{path} + // and otherwise: + // index.php/apps/solid/~{userId}/storage{path} +======= + // because we got here, the request uri should look like: + // /index.php/apps/solid/~{userId}/storage{path} + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^storage/", "", $path); +>>>>>>> 3100599 (replace @ with ~ in urls) + + // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; + if ($this->config->getUserSubDomainsEnabled()) { + $pathInfo = explode("storage/", $_SERVER['REQUEST_URI']); + $path = $pathInfo[1]; + $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; + } else { + $pathInfo = explode("~", $_SERVER['REQUEST_URI']); + $pathInfo = explode("/", $pathInfo[1], 2); + $userId = $pathInfo[0]; + $path = $pathInfo[1]; + $path = preg_replace("/^storage/", "", $path); + } + + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleDelete($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handleHead($userId, $path) { + return $this->handleRequest($userId, $path); + } + /** + * @PublicPage + * @NoAdminRequired + * @NoCSRFRequired + */ + public function handlePatch($userId, $path) { + return $this->handleRequest($userId, $path); + } + + private function respond($response) { + $statusCode = $response->getStatusCode(); + $response->getBody()->rewind(); + $headers = $response->getHeaders(); + + $body = $response->getBody()->getContents(); + + $result = new PlainResponse($body); + + foreach ($headers as $header => $values) { + $result->addHeader($header, implode(", ", $values)); + } + +// $origin = $_SERVER['HTTP_ORIGIN']; +// $result->addHeader('Access-Control-Allow-Credentials', 'true'); +// $result->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); +// $result->addHeader('Access-Control-Allow-Origin', $origin); + + $policy = new EmptyContentSecurityPolicy(); + $policy->addAllowedStyleDomain("*"); + $policy->addAllowedStyleDomain("data:"); + $policy->addAllowedScriptDomain("*"); + $policy->addAllowedImageDomain("*"); + $policy->addAllowedFontDomain("*"); + $policy->addAllowedConnectDomain("*"); + $policy->allowInlineStyle(true); + // $policy->allowInlineScript(true); - removed, this function no longer exists in NC28 + $policy->allowEvalScript(true); + $result->setContentSecurityPolicy($policy); + + $result->setStatus($statusCode); + return $result; + } +} From 2fb8e0e5d0fac3778f954412694087ae3a9f67e1 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 26 May 2025 11:18:26 +0200 Subject: [PATCH 2/5] update pdsinterop packages --- solid/composer.json | 6 +-- solid/composer.lock | 122 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 26 deletions(-) diff --git a/solid/composer.json b/solid/composer.json index 61dbb3b6..3df0b68d 100644 --- a/solid/composer.json +++ b/solid/composer.json @@ -30,9 +30,9 @@ "laminas/laminas-diactoros": "^2.8", "lcobucci/jwt": "^4.1", "pdsinterop/flysystem-nextcloud": "^0.2", - "pdsinterop/flysystem-rdf": "^0.5", - "pdsinterop/solid-auth": "v0.11.0", - "pdsinterop/solid-crud": "^0.7.3", + "pdsinterop/flysystem-rdf": "^0.6", + "pdsinterop/solid-auth": "^0.12.1", + "pdsinterop/solid-crud": "^0.8", "psr/log": "^1.1" }, "require-dev": { diff --git a/solid/composer.lock b/solid/composer.lock index 40fd3303..9b8301f3 100644 --- a/solid/composer.lock +++ b/solid/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1843d50801f15c12e9fb50345b3bfb3b", + "content-hash": "59e7e42f9da02ea5c9908f49de8cdae5", "packages": [ { "name": "arc/base", @@ -1455,24 +1455,24 @@ }, { "name": "pdsinterop/flysystem-rdf", - "version": "v0.5.0", + "version": "v0.6.0", "source": { "type": "git", "url": "https://github.com/pdsinterop/flysystem-rdf.git", - "reference": "2a0b105f66c16b664bcd56f30d76f464b18be065" + "reference": "cb72c2a0538b2a552a9281f2bd9e4a7f48ca035d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdsinterop/flysystem-rdf/zipball/2a0b105f66c16b664bcd56f30d76f464b18be065", - "reference": "2a0b105f66c16b664bcd56f30d76f464b18be065", + "url": "https://api.github.com/repos/pdsinterop/flysystem-rdf/zipball/cb72c2a0538b2a552a9281f2bd9e4a7f48ca035d", + "reference": "cb72c2a0538b2a552a9281f2bd9e4a7f48ca035d", "shasum": "" }, "require": { - "easyrdf/easyrdf": "^1.1.1", "ext-mbstring": "*", "league/flysystem": "^1.0", "ml/json-ld": "^1.2", - "php": "^8.0" + "php": "^8.0", + "sweetrdf/easyrdf": "^1.1" }, "require-dev": { "phpunit/phpunit": "^8|^9" @@ -1490,22 +1490,22 @@ "description": "Flysystem plugin to transform RDF data between various serialization formats.", "support": { "issues": "https://github.com/pdsinterop/flysystem-rdf/issues", - "source": "https://github.com/pdsinterop/flysystem-rdf/tree/v0.5.0" + "source": "https://github.com/pdsinterop/flysystem-rdf/tree/v0.6.0" }, - "time": "2022-08-22T14:36:29+00:00" + "time": "2025-05-16T08:57:11+00:00" }, { "name": "pdsinterop/solid-auth", - "version": "v0.11.0", + "version": "v0.12.1", "source": { "type": "git", "url": "https://github.com/pdsinterop/php-solid-auth.git", - "reference": "0c5f65b0a9340fe9d50bef9d0e279db54610ffac" + "reference": "3674b864cbf8b70fa6da49bb8ee4e0f6c772cbc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdsinterop/php-solid-auth/zipball/0c5f65b0a9340fe9d50bef9d0e279db54610ffac", - "reference": "0c5f65b0a9340fe9d50bef9d0e279db54610ffac", + "url": "https://api.github.com/repos/pdsinterop/php-solid-auth/zipball/3674b864cbf8b70fa6da49bb8ee4e0f6c772cbc1", + "reference": "3674b864cbf8b70fa6da49bb8ee4e0f6c772cbc1", "shasum": "" }, "require": { @@ -1514,7 +1514,7 @@ "ext-openssl": "*", "laminas/laminas-diactoros": "^2.8", "lcobucci/jwt": "^4.1", - "league/oauth2-server": "^8.3.5", + "league/oauth2-server": "^8.5.5", "php": "^8.0", "web-token/jwt-core": "^2.2" }, @@ -1539,22 +1539,22 @@ "description": "OAuth2, OpenID and OIDC for Solid Server implementations.", "support": { "issues": "https://github.com/pdsinterop/php-solid-auth/issues", - "source": "https://github.com/pdsinterop/php-solid-auth/tree/v0.11.0" + "source": "https://github.com/pdsinterop/php-solid-auth/tree/v0.12.1" }, - "time": "2025-02-14T12:57:21+00:00" + "time": "2025-05-18T15:26:58+00:00" }, { "name": "pdsinterop/solid-crud", - "version": "v0.7.3", + "version": "v0.8.0", "source": { "type": "git", "url": "https://github.com/pdsinterop/php-solid-crud.git", - "reference": "c5369ef7b46d3d77a7686c3f4531e818e1797e27" + "reference": "ca1421770b17c69cc5989ce6864e86405030a50c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdsinterop/php-solid-crud/zipball/c5369ef7b46d3d77a7686c3f4531e818e1797e27", - "reference": "c5369ef7b46d3d77a7686c3f4531e818e1797e27", + "url": "https://api.github.com/repos/pdsinterop/php-solid-crud/zipball/ca1421770b17c69cc5989ce6864e86405030a50c", + "reference": "ca1421770b17c69cc5989ce6864e86405030a50c", "shasum": "" }, "require": { @@ -1562,7 +1562,7 @@ "laminas/laminas-diactoros": "^2.14", "league/flysystem": "^1.0", "mjrider/flysystem-factory": "^0.7", - "pdsinterop/flysystem-rdf": "^0.5", + "pdsinterop/flysystem-rdf": "^0.6", "php": "^8.0", "pietercolpaert/hardf": "^0.3", "psr/http-factory": "^1.0", @@ -1586,9 +1586,9 @@ "description": "Solid HTTPS REST API specification compliant implementation for handling Resource CRUD", "support": { "issues": "https://github.com/pdsinterop/php-solid-crud/issues", - "source": "https://github.com/pdsinterop/php-solid-crud/tree/v0.7.3" + "source": "https://github.com/pdsinterop/php-solid-crud/tree/v0.8.0" }, - "time": "2024-01-17T10:48:57+00:00" + "time": "2025-05-16T09:04:57+00:00" }, { "name": "phrity/net-uri", @@ -2128,6 +2128,82 @@ ], "time": "2020-11-03T09:10:25+00:00" }, + { + "name": "sweetrdf/easyrdf", + "version": "1.7", + "source": { + "type": "git", + "url": "https://github.com/sweetrdf/easyrdf.git", + "reference": "6952b79bd1818817f20d0c64de54c7ecd5a24947" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sweetrdf/easyrdf/zipball/6952b79bd1818817f20d0c64de54c7ecd5a24947", + "reference": "6952b79bd1818817f20d0c64de54c7ecd5a24947", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-xmlreader": "*", + "lib-libxml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "ml/json-ld": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "semsol/arc2": "^2.4", + "zendframework/zend-http": "^2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "EasyRdf\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nicholas Humfrey", + "email": "njh@aelius.com", + "homepage": "http://www.aelius.com/njh/", + "role": "Developer" + }, + { + "name": "Alexey Zakhlestin", + "email": "indeyets@gmail.com", + "homepage": "http://indeyets.ru/", + "role": "Developer" + }, + { + "name": "Konrad Abicht", + "email": "hi@inspirito.de", + "homepage": "http://inspirito.de/", + "role": "Maintainer, Developer" + } + ], + "description": "EasyRdf is a PHP library designed to make it easy to consume and produce RDF.", + "keywords": [ + "Linked Data", + "RDF", + "Semantic Web", + "Turtle", + "rdfa", + "sparql" + ], + "support": { + "issues": "https://github.com/sweetrdf/easyrdf/issues", + "source": "https://github.com/sweetrdf/easyrdf/tree/1.7" + }, + "time": "2022-09-19T07:53:57+00:00" + }, { "name": "textalk/websocket", "version": "1.6.3", From c5b40f721536d4265f39bfa39ec7408c3152916b Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Mon, 26 May 2025 14:45:13 +0200 Subject: [PATCH 3/5] update test vars from @alice to ~alice --- env-vars-server.list | 4 ++-- env-vars-testers.list | 10 +++++----- env-vars-thirdparty.list | 2 +- env.list | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/env-vars-server.list b/env-vars-server.list index 28239ba7..1f38d6fe 100644 --- a/env-vars-server.list +++ b/env-vars-server.list @@ -1,6 +1,6 @@ SERVER_ROOT=https://server -STORAGE_ROOT=https://server/apps/solid/@alice/storage/ -ALICE_WEBID=https://server/apps/solid/@alice/profile/card#me +STORAGE_ROOT=https://server/apps/solid/~alice/storage/ +ALICE_WEBID=https://server/apps/solid/~alice/profile/card#me COOKIE_TYPE=nextcloud-compatible USERNAME=alice PASSWORD=alice123 diff --git a/env-vars-testers.list b/env-vars-testers.list index 366167e0..a8a79563 100644 --- a/env-vars-testers.list +++ b/env-vars-testers.list @@ -1,11 +1,11 @@ -WEBID_ALICE=https://server/apps/solid/@alice/profile/card#me +WEBID_ALICE=https://server/apps/solid/~alice/profile/card#me OIDC_ISSUER_ALICE=https://server -STORAGE_ROOT_ALICE=https://server/apps/solid/@alice/storage/ -WEBID_BOB=https://thirdparty/apps/solid/@alice/profile/card#me +STORAGE_ROOT_ALICE=https://server/apps/solid/~alice/storage/ +WEBID_BOB=https://thirdparty/apps/solid/~alice/profile/card#me OIDC_ISSUER_BOB=https://thirdparty STORAGE_ROOT_BOB=https://thirdparty/ -ALICE_WEBID=https://server/apps/solid/@alice/profile/card#me +ALICE_WEBID=https://server/apps/solid/~alice/profile/card#me SERVER_ROOT_ESCAPED=https:\/\/server SERVER_ROOT=https://server -STORAGE_ROOT=https://server/apps/solid/@alice/storage/ +STORAGE_ROOT=https://server/apps/solid/~alice/storage/ SKIP_CONC=1 diff --git a/env-vars-thirdparty.list b/env-vars-thirdparty.list index 9a2c8416..1c889484 100644 --- a/env-vars-thirdparty.list +++ b/env-vars-thirdparty.list @@ -1,5 +1,5 @@ SERVER_ROOT=https://thirdparty -ALICE_WEBID=https://thirdparty/apps/solid/@alice/profile/card#me +ALICE_WEBID=https://thirdparty/apps/solid/~alice/profile/card#me COOKIE_TYPE=nextcloud-compatible USERNAME=alice PASSWORD=alice123 diff --git a/env.list b/env.list index 1256e61d..cef5c00e 100644 --- a/env.list +++ b/env.list @@ -1,2 +1,2 @@ -ALICE_WEBID=https://server/apps/solid/@alice/profile/card#me +ALICE_WEBID=https://server/apps/solid/~alice/profile/card#me COOKIE_TYPE=nextcloud-compatible From cfadbdb188de0a3da9cf6da3fae0c2591b3adcb7 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 30 May 2025 10:55:33 +0200 Subject: [PATCH 4/5] update solid-auth to 0.12.2 --- solid/composer.json | 2 +- solid/composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/solid/composer.json b/solid/composer.json index 3df0b68d..b49ad6ae 100644 --- a/solid/composer.json +++ b/solid/composer.json @@ -31,7 +31,7 @@ "lcobucci/jwt": "^4.1", "pdsinterop/flysystem-nextcloud": "^0.2", "pdsinterop/flysystem-rdf": "^0.6", - "pdsinterop/solid-auth": "^0.12.1", + "pdsinterop/solid-auth": "^0.12.2", "pdsinterop/solid-crud": "^0.8", "psr/log": "^1.1" }, diff --git a/solid/composer.lock b/solid/composer.lock index 9b8301f3..026aa830 100644 --- a/solid/composer.lock +++ b/solid/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "59e7e42f9da02ea5c9908f49de8cdae5", + "content-hash": "630d8401030511a28cf54157d9bbd4cf", "packages": [ { "name": "arc/base", @@ -1496,16 +1496,16 @@ }, { "name": "pdsinterop/solid-auth", - "version": "v0.12.1", + "version": "v0.12.2", "source": { "type": "git", "url": "https://github.com/pdsinterop/php-solid-auth.git", - "reference": "3674b864cbf8b70fa6da49bb8ee4e0f6c772cbc1" + "reference": "1d1160ee0f7ca71d3e34151aea94232e1cfa49ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdsinterop/php-solid-auth/zipball/3674b864cbf8b70fa6da49bb8ee4e0f6c772cbc1", - "reference": "3674b864cbf8b70fa6da49bb8ee4e0f6c772cbc1", + "url": "https://api.github.com/repos/pdsinterop/php-solid-auth/zipball/1d1160ee0f7ca71d3e34151aea94232e1cfa49ff", + "reference": "1d1160ee0f7ca71d3e34151aea94232e1cfa49ff", "shasum": "" }, "require": { @@ -1539,9 +1539,9 @@ "description": "OAuth2, OpenID and OIDC for Solid Server implementations.", "support": { "issues": "https://github.com/pdsinterop/php-solid-auth/issues", - "source": "https://github.com/pdsinterop/php-solid-auth/tree/v0.12.1" + "source": "https://github.com/pdsinterop/php-solid-auth/tree/v0.12.2" }, - "time": "2025-05-18T15:26:58+00:00" + "time": "2025-05-28T14:53:41+00:00" }, { "name": "pdsinterop/solid-crud", From 52d734e2fbc259492da018427676519d777e7c13 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Fri, 30 May 2025 11:24:05 +0200 Subject: [PATCH 5/5] remove backup files --- solid/lib/Controller/AppController.php~ | 99 ---- solid/lib/Controller/CalendarController.php~ | 278 ----------- solid/lib/Controller/ContactsController.php~ | 279 ----------- solid/lib/Controller/GetStorageUrlTrait.php~ | 91 ---- solid/lib/Controller/PageController.php~ | 152 ------ solid/lib/Controller/ProfileController.php~ | 378 --------------- solid/lib/Controller/ServerController.php~ | 449 ------------------ solid/lib/Controller/StorageController.php~ | 471 ------------------- 8 files changed, 2197 deletions(-) delete mode 100644 solid/lib/Controller/AppController.php~ delete mode 100644 solid/lib/Controller/CalendarController.php~ delete mode 100644 solid/lib/Controller/ContactsController.php~ delete mode 100644 solid/lib/Controller/GetStorageUrlTrait.php~ delete mode 100644 solid/lib/Controller/PageController.php~ delete mode 100644 solid/lib/Controller/ProfileController.php~ delete mode 100644 solid/lib/Controller/ServerController.php~ delete mode 100644 solid/lib/Controller/StorageController.php~ diff --git a/solid/lib/Controller/AppController.php~ b/solid/lib/Controller/AppController.php~ deleted file mode 100644 index d0b99e92..00000000 --- a/solid/lib/Controller/AppController.php~ +++ /dev/null @@ -1,99 +0,0 @@ -userId = $userId; - $this->userManager = $userManager; - $this->contactsManager = $contactsManager; - $this->request = $request; - $this->urlGenerator = $urlGenerator; - $this->config = new ServerConfig($config, $urlGenerator, $userManager); - } - - private function getUserApps($userId) { - $userApps = []; - if ($this->userManager->userExists($userId)) { - $allowedClients = $this->config->getAllowedClients($userId); - foreach ($allowedClients as $clientId) { - $registration = $this->config->getClientRegistration($clientId); - $userApps[] = $registration['client_name']; - } - } - return $userApps; - } - - private function getAppsList() { - $path = __DIR__ . "/../solid-app-list.json"; - $appsListJson = file_get_contents($path); - $appsList = json_decode($appsListJson, true); - - $userApps = $this->getUserApps($this->userId); - - foreach ($appsList as $key => $app) { - $parsedOrigin = parse_url($app['launchUrl']); - $origin = $parsedOrigin['host']; - if (in_array($origin, $userApps, true)) { - $appsList[$key]['registered'] = 1; - } else { - $appsList[$key]['registered'] = 0; - } - } - return $appsList; - } - - private function getProfilePage() { - return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $this->userId, "path" => "/card"))) . "#me"; - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function appLauncher() { - $appsList = $this->getAppsList(); - if (!$appsList) { - return new JSONResponse(array(), Http::STATUS_NOT_FOUND); - } - $appLauncherData = array( - "appsListJson" => json_encode($appsList), - "webId" => json_encode($this->getProfilePage()), - "storageUrl" => json_encode($this->getStorageUrl($this->userId)), - 'solidNavigation' => array( - "profile" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.profile", array("userId" => $this->userId))), - "launcher" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.app.appLauncher", array())), - ) - ); - $templateResponse = new TemplateResponse('solid', 'applauncher', $appLauncherData); - $policy = new ContentSecurityPolicy(); - $policy->addAllowedStyleDomain("data:"); - $policy->addAllowedScriptDomain("'self'"); - $policy->addAllowedScriptDomain("'unsafe-inline'"); - $policy->addAllowedScriptDomain("'unsafe-eval'"); - $templateResponse->setContentSecurityPolicy($policy); - return $templateResponse; - } -} diff --git a/solid/lib/Controller/CalendarController.php~ b/solid/lib/Controller/CalendarController.php~ deleted file mode 100644 index 439dcdb0..00000000 --- a/solid/lib/Controller/CalendarController.php~ +++ /dev/null @@ -1,278 +0,0 @@ -config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); - $this->request = $request; - $this->urlGenerator = $urlGenerator; - $this->session = $session; - - $this->setJtiStorage($connection); - } - - private function getFileSystem($userId) { - // Make sure the root folder has an acl file, as is required by the spec; - // Generate a default file granting the owner full access. - $defaultAcl = $this->generateDefaultAcl($userId); - - // Create the Nextcloud Calendar Adapter - $adapter = new \Pdsinterop\Flysystem\Adapter\NextcloudCalendar($userId, $defaultAcl); - - $graph = new \EasyRdf\Graph(); - - // Create Formats objects - $formats = new \Pdsinterop\Rdf\Formats(); - - $serverParams = $this->rawRequest->getServerParams(); - $scheme = $serverParams['REQUEST_SCHEME']; - $domain = $serverParams['SERVER_NAME']; - $path = $serverParams['REQUEST_URI']; - $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; - - // Create the RDF Adapter - $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( - $adapter, - $graph, - $formats, - $serverUri - ); - - $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); - - $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - - $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); - $filesystem->addPlugin($plugin); - - return $filesystem; - } - - private function generateDefaultAcl($userId) { - $defaultAcl = <<< EOF -# Root ACL resource for the user account -@prefix acl: . -@prefix foaf: . - -# The owner has full access to every resource in their pod. -# Other agents have no access rights, -# unless specifically authorized in other .acl resources. -<#owner> - a acl:Authorization; - acl:agent <{user-profile-uri}>; - # Set the access to the root storage folder itself - acl:accessTo <./>; - # All resources will inherit this authorization, by default - acl:default <./>; - # The owner has all of the access modes allowed - acl:mode - acl:Read, acl:Write, acl:Control. -EOF; - - $profileUri = $this->getUserProfile($userId); - $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); - return $defaultAcl; - } - - private function getUserProfile($userId) { - return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; - } - private function getCalendarUrl($userId) { - $calendarUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.calendar.handleHead", array("userId" => $userId, "path" => "foo"))); - $calendarUrl = preg_replace('/foo$/', '', $calendarUrl); - return $calendarUrl; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleRequest($userId, $path) { - $this->calendarUserId = $userId; - - $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $this->response = new \Laminas\Diactoros\Response(); - - $this->filesystem = $this->getFileSystem($userId); - - $this->resourceServer = new ResourceServer($this->filesystem, $this->response); - $this->WAC = new WAC($this->filesystem); - - $request = $this->rawRequest; - $baseUrl = $this->getCalendarUrl($userId); - $this->resourceServer->setBaseUrl($baseUrl); - $this->WAC->setBaseUrl($baseUrl); - $notifications = new SolidNotifications(); - $this->resourceServer->setNotifications($notifications); - - $dpop = $this->getDpop(); - - try { - $webId = $dpop->getWebId($request); - } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { - $response = $this->resourceServer->getResponse() - ->withStatus(Http::STATUS_CONFLICT, "Invalid token " . $e->getMessage()); - return $this->respond($response); - } - - if (!$this->WAC->isAllowed($request, $webId)) { - $response = $this->resourceServer->getResponse()->withStatus(403, "Access denied"); - return $this->respond($response); - } - - $response = $this->resourceServer->respondToRequest($request); - $response = $this->WAC->addWACHeaders($request, $response, $webId); - return $this->respond($response); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleGet($userId, $path) { - return $this->handleRequest($userId, $path); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePost($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ -<<<<<<< HEAD - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/calendar{path} - // and otherwise: - // index.php/apps/solid/~{userId}/calendar{path} - - // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("calendar/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^calendar/", "", $path); - } - - return $this->handleRequest($userId, $path); - } -======= - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - - // because we got here, the request uri should look like: - // /index.php/apps/solid/~{userId}/storage{path} - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^calendar/", "", $path); - - return $this->handleRequest($userId, $path); - } ->>>>>>> 3100599 (replace @ with ~ in urls) - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleDelete($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleHead($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePatch($userId, $path) { - return $this->handleRequest($userId, $path); - } - - private function respond($response) { - $statusCode = $response->getStatusCode(); - $response->getBody()->rewind(); - $headers = $response->getHeaders(); - - $body = $response->getBody()->getContents(); - if ($statusCode > 399) { - $reason = $response->getReasonPhrase(); - $result = new JSONResponse($reason, $statusCode); - return $result; - } - - $result = new PlainResponse($body); - - foreach ($headers as $header => $values) { - foreach ($values as $value) { - $result->addHeader($header, $value); - } - } - - $result->setStatus($statusCode); - return $result; - } -} diff --git a/solid/lib/Controller/ContactsController.php~ b/solid/lib/Controller/ContactsController.php~ deleted file mode 100644 index f346036e..00000000 --- a/solid/lib/Controller/ContactsController.php~ +++ /dev/null @@ -1,279 +0,0 @@ -config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); - $this->request = $request; - $this->urlGenerator = $urlGenerator; - $this->session = $session; - - $this->setJtiStorage($connection); - } - - private function getFileSystem($userId) { - // Make sure the root folder has an acl file, as is required by the spec; - // Generate a default file granting the owner full access. - $defaultAcl = $this->generateDefaultAcl($userId); - - // Create the Nextcloud Contacts Adapter - $adapter = new \Pdsinterop\Flysystem\Adapter\NextcloudContacts($userId, $defaultAcl); - - $graph = new \EasyRdf\Graph(); - - // Create Formats objects - $formats = new \Pdsinterop\Rdf\Formats(); - - $serverParams = $this->rawRequest->getServerParams(); - $scheme = $serverParams['REQUEST_SCHEME']; - $domain = $serverParams['SERVER_NAME']; - $path = $serverParams['REQUEST_URI']; - $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; - - // Create the RDF Adapter - $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( - $adapter, - $graph, - $formats, - $serverUri - ); - - $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); - - $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - - $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); - $filesystem->addPlugin($plugin); - - return $filesystem; - } - - private function generateDefaultAcl($userId) { - $defaultAcl = <<< EOF -# Root ACL resource for the user account -@prefix acl: . -@prefix foaf: . - -# The owner has full access to every resource in their pod. -# Other agents have no access rights, -# unless specifically authorized in other .acl resources. -<#owner> - a acl:Authorization; - acl:agent <{user-profile-uri}>; - # Set the access to the root storage folder itself - acl:accessTo <./>; - # All resources will inherit this authorization, by default - acl:default <./>; - # The owner has all of the access modes allowed - acl:mode - acl:Read, acl:Write, acl:Control. -EOF; - - $profileUri = $this->getUserProfile($userId); - $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); - return $defaultAcl; - } - - private function getUserProfile($userId) { - return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; - } - private function getContactsUrl($userId) { - $contactsUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.contacts.handleHead", array("userId" => $userId, "path" => "foo"))); - $contactsUrl = preg_replace('/foo$/', '', $contactsUrl); - return $contactsUrl; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleRequest($userId, $path) { - $this->contactsUserId = $userId; - - $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $this->response = new \Laminas\Diactoros\Response(); - - $this->filesystem = $this->getFileSystem($userId); - - $this->resourceServer = new ResourceServer($this->filesystem, $this->response); - $this->WAC = new WAC($this->filesystem); - - $request = $this->rawRequest; - $baseUrl = $this->getContactsUrl($userId); - $this->resourceServer->setBaseUrl($baseUrl); - $this->WAC->setBaseUrl($baseUrl); - $notifications = new SolidNotifications(); - $this->resourceServer->setNotifications($notifications); - - $dpop = $this->getDpop(); - - try { - $webId = $dpop->getWebId($request); - } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { - $response = $this->resourceServer->getResponse() - ->withStatus(Http::STATUS_CONFLICT, "Invalid token " . $e->getMessage()); - return $this->respond($response); - } - - if (!$this->WAC->isAllowed($request, $webId)) { - $response = $this->resourceServer->getResponse()->withStatus(403, "Access denied"); - return $this->respond($response); - } - - $response = $this->resourceServer->respondToRequest($request); - $response = $this->WAC->addWACHeaders($request, $response, $webId); - return $this->respond($response); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleGet($userId, $path) { - return $this->handleRequest($userId, $path); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePost($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ -<<<<<<< HEAD - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/contacts{path} - // and otherwise: - // index.php/apps/solid/~{userId}/contacts{path} - - // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("contacts/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^contacts/", "", $path); - } - - return $this->handleRequest($userId, $path); - } -======= - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - - // because we got here, the request uri should look like: - // /index.php/apps/solid/~{userId}/storage{path} - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^contacts/", "", $path); - - return $this->handleRequest($userId, $path); - } ->>>>>>> 3100599 (replace @ with ~ in urls) - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleDelete($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleHead($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePatch($userId, $path) { - return $this->handleRequest($userId, $path); - } - - private function respond($response) { - $statusCode = $response->getStatusCode(); - $response->getBody()->rewind(); - $headers = $response->getHeaders(); - - $body = $response->getBody()->getContents(); - if ($statusCode > 399) { - $reason = $response->getReasonPhrase(); - $result = new JSONResponse($reason, $statusCode); - return $result; - } - - $result = new PlainResponse($body); - - foreach ($headers as $header => $values) { - foreach ($values as $value) { - $result->addHeader($header, $value); - } - } - - $result->setStatus($statusCode); - return $result; - } -} diff --git a/solid/lib/Controller/GetStorageUrlTrait.php~ b/solid/lib/Controller/GetStorageUrlTrait.php~ deleted file mode 100644 index 97a312b7..00000000 --- a/solid/lib/Controller/GetStorageUrlTrait.php~ +++ /dev/null @@ -1,91 +0,0 @@ -config = $config; - } - - final public function setUrlGenerator(IURLGenerator $urlGenerator): void - { - $this->urlGenerator = $urlGenerator; - } - - ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ - - protected ServerConfig $config; - protected IURLGenerator $urlGenerator; - - /////////////////////////////// PROTECTED API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ - - /** - * @FIXME: Add check for bob.nextcloud.local/solid/alice to throw 404 - * @TODO: Use route without `@alice` in /apps/solid/@alice/profile/card#me when user-domains are enabled - */ - public function getStorageUrl($userId) { - $routeUrl = $this->urlGenerator->linkToRoute( - 'solid.storage.handleHead', - ['userId' => $userId, 'path' => 'foo'] - ); - - $storageUrl = $this->urlGenerator->getAbsoluteURL($routeUrl); - - // (?) $storageUrl = preg_replace('/foo$/', '', $storageUrl); - $storageUrl = preg_replace('/foo$/', '/', $storageUrl); - - if ($this->config->getUserSubDomainsEnabled()) { - $url = parse_url($storageUrl); - - if (strpos($url['host'], $userId . '.') !== false) { - $url['host'] = str_replace($userId . '.', '', $url['host']); - } - - $url['host'] = $userId . '.' . $url['host']; // $storageUrl = $userId . '.' . $storageUrl; - $storageUrl = $this->build_url($url); - } - - return $storageUrl; - } - - public function validateUrl(RequestInterface $request): bool { - $isValid = false; - - $host = $request->getUri()->getHost(); - $path = $request->getUri()->getPath(); - $pathParts = explode('/', $path); - - $pathUsers = array_filter($pathParts, static function ($value) { - return str_starts_with($value, '@'); - }); - - if (count($pathUsers) === 1) { - $pathUser = reset($pathUsers); - $subDomainUser = explode('.', $host)[0]; - - $isValid = $pathUser === '@' . $subDomainUser; - } - - return $isValid; - } - - ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ - - private function build_url(array $parts) { - // @FIXME: Replace with existing more robust URL builder - return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') . - (isset($parts['host']) ? "//{$parts['host']}" : '') . - (isset($parts['port']) ? ":{$parts['port']}" : '') . - (isset($parts['path']) ? "{$parts['path']}" : '') . - (isset($parts['query']) ? "?{$parts['query']}" : '') . - (isset($parts['fragment']) ? "#{$parts['fragment']}" : ''); - } -} diff --git a/solid/lib/Controller/PageController.php~ b/solid/lib/Controller/PageController.php~ deleted file mode 100644 index 3109ad8e..00000000 --- a/solid/lib/Controller/PageController.php~ +++ /dev/null @@ -1,152 +0,0 @@ -userId = $userId; - $this->userManager = $userManager; - $this->request = $request; - $this->urlGenerator = $urlGenerator; - $this->config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); - } - - /** - * CAUTION: the @Stuff turns off security checks; for this page no admin is - * required and no CSRF check. If you don't know what CSRF is, read - * it up in the docs or you might create a security hole. This is - * basically the only required method to add this exemption, don't - * add it to any other method if you don't exactly know what it does - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function index() { - return new TemplateResponse('solid', 'index'); // templates/index.php - } - - private function getUserProfile($userId) { - if ($this->userManager->userExists($userId)) { - $user = $this->userManager->get($userId); - if ($user !== null) { - $profile = array( - 'id' => $userId, - 'displayName' => $user->getDisplayName(), - 'profileUri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me", - 'solidNavigation' => array( - "profile" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.profile", array("userId" => $userId))), - "launcher" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.app.appLauncher", array())), - ) - ); - return $profile; - } - } - return false; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function profile($userId) { - // header("Access-Control-Allow-Headers: *, authorization, accept, content-type"); - // header("Access-Control-Allow-Credentials: true"); - $profile = $this->getUserProfile($userId); - if (!$profile) { - return new JSONResponse(array(), Http::STATUS_NOT_FOUND); - } - $templateResponse = new TemplateResponse('solid', 'profile', $profile); - $policy = new ContentSecurityPolicy(); - $policy->addAllowedStyleDomain("data:"); - $templateResponse->setContentSecurityPolicy($policy); - return $templateResponse; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function approval($clientId) { - $clientRegistration = $this->config->getClientRegistration($clientId); - $params = array( - "clientId" => $clientId, - "clientName" => $clientRegistration['client_name'], - "serverName" => "Nextcloud", - "returnUrl" => $_GET['returnUrl'], - ); - $templateResponse = new TemplateResponse('solid', 'sharing', $params); - - $policy = new ContentSecurityPolicy(); - $policy->addAllowedStyleDomain("data:"); - - $parsedOrigin = parse_url($clientRegistration['redirect_uris'][0]); - $origin = $parsedOrigin['host']; - if ($origin) { - $policy->addAllowedFormActionDomain($parsedOrigin['scheme'] . "://" . $origin); - $templateResponse->setContentSecurityPolicy($policy); - } - return $templateResponse; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function customscheme() { - $templateResponse = new TemplateResponse('solid', 'customscheme'); - return $templateResponse; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleApproval($clientId) { - $approval = $_POST['approval']; - if ($approval == "allow") { - $this->config->addAllowedClient($this->userId, $clientId); - } else { - $this->config->removeAllowedClient($this->userId, $clientId); - } - $authUrl = $_POST['returnUrl']; - - $result = new JSONResponse("ok"); - - $result->setStatus("302"); - $result->addHeader("Location", $authUrl); - return $result; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleRevoke($clientId) { - $this->config->removeAllowedClient($this->userId, $clientId); - $result = new JSONResponse("ok"); - return $result; - } -} diff --git a/solid/lib/Controller/ProfileController.php~ b/solid/lib/Controller/ProfileController.php~ deleted file mode 100644 index 70ba5e12..00000000 --- a/solid/lib/Controller/ProfileController.php~ +++ /dev/null @@ -1,378 +0,0 @@ -config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); - $this->request = $request; - $this->urlGenerator = $urlGenerator; - $this->userManager = $userManager; - $this->contactsManager = $contactsManager; - $this->session = $session; - - $this->setJtiStorage($connection); - } - - private function getFileSystem($userId) { - // Make sure the root folder has an acl file, as is required by the spec; - // Generate a default file granting the owner full access. - $defaultAcl = $this->generateDefaultAcl($userId); - $profile = $this->generateTurtleProfile($userId); - - // Create the Nextcloud Calendar Adapter - $adapter = new \Pdsinterop\Flysystem\Adapter\NextcloudProfile($userId, $profile, $defaultAcl, $this->config); - - $graph = new \EasyRdf\Graph(); - // Create Formats objects - $formats = new \Pdsinterop\Rdf\Formats(); - - $serverParams = $this->rawRequest->getServerParams(); - $scheme = $serverParams['REQUEST_SCHEME']; - $domain = $serverParams['SERVER_NAME']; - $path = $serverParams['REQUEST_URI']; - $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; - - // Create the RDF Adapter - $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( - $adapter, - $graph, - $formats, - $serverUri - ); - - $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); - - $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - - $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); - $filesystem->addPlugin($plugin); - - return $filesystem; - } - - private function generateDefaultAcl($userId) { - $defaultAcl = <<< EOF -# Root ACL resource for the user account -@prefix acl: . -@prefix foaf: . - -# The profile is readable by the public -<#public> - a acl:Authorization; - acl:agentClass foaf:Agent; - acl:accessTo <./>; - acl:default <./>; - acl:mode acl:Read. - -# The owner has full access to every resource in their pod. -# Other agents have no access rights, -# unless specifically authorized in other .acl resources. -<#owner> - a acl:Authorization; - acl:agent <{user-profile-uri}>; - # Set the access to the root storage folder itself - acl:accessTo <./>; - # All resources will inherit this authorization, by default - acl:default <./>; - # The owner has all of the access modes allowed - acl:mode - acl:Read, acl:Write, acl:Control. -EOF; - - $profileUri = $this->getUserProfileUri($userId); - $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); - return $defaultAcl; - } - - private function getUserProfileUri($userId) { - return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; - } - private function getProfileUrl($userId) { - $profileUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleHead", array("userId" => $userId, "path" => "foo"))); - $profileUrl = preg_replace('/foo$/', '', $profileUrl); - return $profileUrl; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleRequest($userId, $path) { - $this->userId = $userId; - - $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $this->response = new \Laminas\Diactoros\Response(); - - $this->filesystem = $this->getFileSystem($userId); - - $this->resourceServer = new ResourceServer($this->filesystem, $this->response); - $this->WAC = new WAC($this->filesystem); - - $request = $this->rawRequest; - $baseUrl = $this->getProfileUrl($userId); - $this->resourceServer->setBaseUrl($baseUrl); - $this->WAC->setBaseUrl($baseUrl); - $notifications = new SolidNotifications(); - $this->resourceServer->setNotifications($notifications); - - $dpop = $this->getDpop(); - - if ($request->getHeaderLine("DPop")) { - try { - $webId = $dpop->getWebId($request); - } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { - $response = $this->resourceServer->getResponse() - ->withStatus(Http::STATUS_CONFLICT, "Invalid token " . $e->getMessage()); - return $this->respond($response); - } - } else { - $webId = ""; - } - - if (!$this->WAC->isAllowed($request, $webId)) { - $response = $this->resourceServer->getResponse()->withStatus(403, "Access denied"); - return $this->respond($response); - } - - $response = $this->resourceServer->respondToRequest($request); - $response = $this->WAC->addWACHeaders($request, $response, $webId); - return $this->respond($response); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleGet($userId, $path) { - //TODO: check that the $userId matches the userDomain, if enabled. - return $this->handleRequest($userId, $path); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePost($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - -<<<<<<< HEAD - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/profile{path} - // and otherwise: - // index.php/apps/solid/~{userId}/profile{path} -======= - // because we got here, the request uri should look like: - // /index.php/apps/solid/~{userId}/storage{path} - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^profile/", "", $path); ->>>>>>> 3100599 (replace @ with ~ in urls) - - // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("profile/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^profile/", "", $path); - } - - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleDelete($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleHead($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePatch($userId, $path) { - return $this->handleRequest($userId, $path); - } - - private function respond($response) { - $statusCode = $response->getStatusCode(); - $response->getBody()->rewind(); - $headers = $response->getHeaders(); - - $body = $response->getBody()->getContents(); - if ($statusCode > 399) { - $reason = $response->getReasonPhrase(); - $result = new JSONResponse($reason, $statusCode); - return $result; - } - - $result = new PlainResponse($body); - - foreach ($headers as $header => $values) { - foreach ($values as $value) { - $result->addHeader($header, $value); - } - } -// $origin = $_SERVER['HTTP_ORIGIN'] ?? "*"; -// $result->addHeader('Access-Control-Allow-Credentials', 'true'); -// $result->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); -// $result->addHeader('Access-Control-Allow-Origin', $origin); - $result->setStatus($statusCode); - return $result; - } - - private function getUserProfile($userId) { - if ($this->userManager->userExists($userId)) { - $user = $this->userManager->get($userId); - $addressBooks = $this->contactsManager->getUserAddressBooks(); - $friends = []; - foreach($addressBooks as $k => $v) { - $results = $addressBooks[$k]->search('', ['FN'], ['types' => true]); - foreach($results as $found) { - if (isset($found['URL']) && is_array($found['URL'])) { - foreach($found['URL'] as $i => $obj) { - array_push($friends, $obj['value']); - } - } - } - } - //TODO: privateTypeIndex and publisTypeIndex need to user getStorageURL - if ($user !== null) { - $profile = array( - 'id' => $userId, - 'displayName' => $user->getDisplayName(), - 'profileUri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me", - 'friends' => $friends, - 'inbox' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/inbox/"))), - 'preferences' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/preferences.ttl"))), - 'privateTypeIndex' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/privateTypeIndex.ttl"))), - 'publicTypeIndex' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.storage.handleGet", array("userId" => $userId, "path" => "/settings/publicTypeIndex.ttl"))), - 'storage' => $this->getStorageUrl($userId), - 'issuer' => $this->urlGenerator->getBaseURL() - ); - return $profile; - } - } - return false; - } - - private function generateTurtleProfile($userId) { - $profile = $this->getUserProfile($userId); - if (!$profile) { - return ""; - } - ob_start(); - ?>@prefix : <#>. - @prefix solid: . - @prefix pro: <./>. - @prefix foaf: . - @prefix schem: . - @prefix acl: . - @prefix ldp: . - @prefix inbox: <>. - @prefix sp: . - @prefix ser: <>. - - pro:card a foaf:PersonalProfileDocument; foaf:maker :me; foaf:primaryTopic :me. - - :me - a schem:Person, foaf:Person; - ldp:inbox inbox:; - sp:preferencesFile <>; - sp:storage ser:; - solid:account ser:; - solid:privateTypeIndex <>; - solid:publicTypeIndex <>; - solid:oidcIssuer <>; - $friend) { - ?> - foaf:knows <>; - - foaf:name ""; - "". - urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))); - $baseProfile = $this->config->getProfileData($userId); - $graph = new \EasyRdf\Graph(); - $graph->parse($baseProfile, "turtle", $baseUrl); - $graph->parse($generatedProfile, "turtle", $baseUrl); - $combinedProfile = $graph->serialise("turtle"); - return $combinedProfile; - } -} diff --git a/solid/lib/Controller/ServerController.php~ b/solid/lib/Controller/ServerController.php~ deleted file mode 100644 index 9c9044ee..00000000 --- a/solid/lib/Controller/ServerController.php~ +++ /dev/null @@ -1,449 +0,0 @@ -config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); - $this->userId = $userId; - $this->userManager = $userManager; - $this->request = $request; - $this->urlGenerator = $urlGenerator; - $this->session = $session; - - $this->setJtiStorage($connection); - - $this->authServerConfig = $this->createAuthServerConfig(); - $this->authServerFactory = (new \Pdsinterop\Solid\Auth\Factory\AuthorizationServerFactory($this->authServerConfig))->create(); - - $this->tokenGenerator = new \Pdsinterop\Solid\Auth\TokenGenerator( - $this->authServerConfig, - $this->getDpopValidFor(), - $this->getDpop() - ); - } - - private function getOpenIdEndpoints() { - return [ - 'issuer' => $this->urlGenerator->getBaseURL(), - 'authorization_endpoint' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.authorize")), - 'jwks_uri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.jwks")), - "check_session_iframe" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.session")), - "end_session_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.logout")), - "token_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.token")), - "userinfo_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.userinfo")), - "registration_endpoint" => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.register")) - ]; - } - - private function getKeys() { - $encryptionKey = $this->config->getEncryptionKey(); - $privateKey = $this->config->getPrivateKey(); - $key = openssl_pkey_get_private($privateKey); - $publicKey = openssl_pkey_get_details($key)['key']; - return [ - "encryptionKey" => $encryptionKey, - "privateKey" => $privateKey, - "publicKey" => $publicKey - ]; - } - - private function createAuthServerConfig() { - $clientId = isset($_GET['client_id']) ? $_GET['client_id'] : null; - $client = $this->getClient($clientId); - $keys = $this->getKeys(); - try { - return (new \Pdsinterop\Solid\Auth\Factory\ConfigFactory( - $client, - $keys['encryptionKey'], - $keys['privateKey'], - $keys['publicKey'], - $this->getOpenIdEndpoints() - ))->create(); - } catch(\Throwable $e) { - // var_dump($e); - return null; - } - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function cors($path) { - $origin = $_SERVER['HTTP_ORIGIN']; - return (new DataResponse('OK')); -// ->addHeader('Access-Control-Allow-Origin', $origin) -// ->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization') -// ->addHeader('Access-Control-Allow-Methods', 'POST') -// ->addHeader('Access-Control-Allow-Credentials', 'true'); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function authorize() { - // Create a request - if (!$this->userManager->userExists($this->userId)) { - $result = new JSONResponse('Authorization required'); - $result->setStatus(401); - return $result; -// return $result->addHeader('Access-Control-Allow-Origin', '*'); - } - - if (isset($_GET['request'])) { - $jwtConfig = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($this->config->getPrivateKey())); - try { - $token = $jwtConfig->parser()->parse($_GET['request']); - $this->session->set("nonce", $token->claims()->get('nonce')); - } catch(\Exception $e) { - $this->session->set("nonce", $_GET['nonce']); - } - } - - $getVars = $_GET; - if (!isset($getVars['grant_type'])) { - $getVars['grant_type'] = 'implicit'; - } - $getVars['response_type'] = $this->getResponseType(); - $getVars['scope'] = "openid" ; - - if (!isset($getVars['redirect_uri'])) { - if (!isset($token)) { - $result = new JSONResponse('Bad request, does not contain valid token'); - $result->setStatus(400); - return $result; -// return $result->addHeader('Access-Control-Allow-Origin', '*'); - } - try { - $getVars['redirect_uri'] = $token->claims()->get("redirect_uri"); - } catch(\Exception $e) { - $result = new JSONResponse('Bad request, missing redirect uri'); - $result->setStatus(400); - return $result; -// return $result->addHeader('Access-Control-Allow-Origin', '*'); - } - } - - if (preg_match("/^http(s)?:/", $getVars['client_id'])) { - $parsedOrigin = parse_url($getVars['redirect_uri']); - $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; - if (isset($parsedOrigin['port'])) { - $origin .= ":" . $parsedOrigin['port']; - } - $clientData = array( - "client_id_issued_at" => time(), - "client_name" => $getVars['client_id'], - "origin" => $origin, - "redirect_uris" => array( - $getVars['redirect_uri'] - ) - ); - $clientId = $this->config->saveClientRegistration($origin, $clientData)['client_id']; - $clientId = $this->config->saveClientRegistration($getVars['client_id'], $clientData)['client_id']; - $returnUrl = $getVars['redirect_uri']; - } else { - $clientId = $getVars['client_id']; - $returnUrl = $_SERVER['REQUEST_URI']; - } - - $clientRegistration = $this->config->getClientRegistration($clientId); - if (isset($clientRegistration['blocked']) && ($clientRegistration['blocked'] === true)) { - $result = new JSONResponse('Unauthorized client'); - $result->setStatus(403); - return $result; - } - - $approval = $this->checkApproval($clientId); - if (!$approval) { - $result = new JSONResponse('Approval required'); - $result->setStatus(302); - $approvalUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.approval", array("clientId" => $clientId, "returnUrl" => $returnUrl))); - $result->addHeader("Location", $approvalUrl); - return $result; // ->addHeader('Access-Control-Allow-Origin', '*'); - } - - $parsedOrigin = parse_url($clientRegistration['redirect_uris'][0]); - if ($parsedOrigin['scheme'] != "https" && !isset($_GET['customscheme'])) { - $result = new JSONResponse('Custom schema'); - $result->setStatus(302); - $originalRequest = parse_url($_SERVER['REQUEST_URI']); - $customSchemeUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.page.customscheme")) . ($originalRequest['query'] ? "?" . $originalRequest['query'] . "&customscheme=" . $parsedOrigin['scheme'] : ''); - $result->addHeader("Location", $customSchemeUrl); - return $result; - } - - $user = new \Pdsinterop\Solid\Auth\Entity\User(); - $user->setIdentifier($this->getProfilePage()); - - $request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $getVars, $_POST, $_COOKIE, $_FILES); - $response = new \Laminas\Diactoros\Response(); - $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); - - $response = $server->respondToAuthorizationRequest($request, $user, $approval); - $response = $this->tokenGenerator->addIdTokenToResponse( - $response, - $clientId, - $this->getProfilePage(), - $this->session->get("nonce"), - $this->config->getPrivateKey() - ); - - return $this->respond($response); // ->addHeader('Access-Control-Allow-Origin', '*'); - } - - private function checkApproval($clientId) { - $allowedClients = $this->config->getAllowedClients($this->userId); - if ($clientId == md5("tester")) { // FIXME: Double check that this is not a security issue; It is only here to help the test suite; - return \Pdsinterop\Solid\Auth\Enum\Authorization::APPROVED; - } - if ($clientId == md5("https://tester")) { // FIXME: Double check that this is not a security issue; It is only here to help the test suite; - return \Pdsinterop\Solid\Auth\Enum\Authorization::APPROVED; - } - if (in_array($clientId, $allowedClients)) { - return \Pdsinterop\Solid\Auth\Enum\Authorization::APPROVED; - } else { - return \Pdsinterop\Solid\Auth\Enum\Authorization::DENIED; - } - } - - private function getProfilePage() { - return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $this->userId, "path" => "/card"))) . "#me"; - } - - private function getResponseType() { - $responseTypes = explode(" ", $_GET['response_type']); - foreach ($responseTypes as $responseType) { - switch ($responseType) { - case "token": - return "token"; - break; - case "code": - return "code"; - break; - } - } - return "token"; // default to token response type; - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function session() { - return new JSONResponse("ok"); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function token() { - $request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $code = $request->getParsedBody()['code']; - $clientId = $request->getParsedBody()['client_id']; - - $httpDpop = $request->getServerParams()['HTTP_DPOP']; - - $response = new \Laminas\Diactoros\Response(); - $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); - $response = $server->respondToAccessTokenRequest($request); - - // FIXME: not sure if decoding this here is the way to go. - // FIXME: because this is a public page, the nonce from the session is not available here. - $codeInfo = $this->tokenGenerator->getCodeInfo($code); - $response = $this->tokenGenerator->addIdTokenToResponse( - $response, - $clientId, - $codeInfo['user_id'], - ($_SESSION['nonce'] ?? ''), - $this->config->getPrivateKey(), - $httpDpop - ); - - return $this->respond($response); // ->addHeader('Access-Control-Allow-Origin', '*'); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function userinfo() { - return new JSONResponse("ok"); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function logout() { - $this->userService->logout(); - return new JSONResponse("ok"); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function register() { - $clientData = file_get_contents('php://input'); - $clientData = json_decode($clientData, true); - if (!$clientData['redirect_uris']) { - return new JSONResponse("Missing redirect URIs"); - } - $clientData['client_id_issued_at'] = time(); - $parsedOrigin = parse_url($clientData['redirect_uris'][0]); - $origin = $parsedOrigin['scheme'] . '://' . $parsedOrigin['host']; - if (isset($parsedOrigin['port'])) { - $origin .= ":" . $parsedOrigin['port']; - } - - $clientData = $this->config->saveClientRegistration($origin, $clientData); - $registration = array( - 'client_id' => $clientData['client_id'], - /* - FIXME: returning client_secret will trigger calls with basic auth to us. To get this to work, we need this patch: - // File /var/www/vhosts/solid-nextcloud/site/www/lib/base.php not changed so no update needed - // ($request->getRawPathInfo() !== '/apps/oauth2/api/v1/token') && - // ($request->getRawPathInfo() !== '/apps/solid/token') - */ - // 'client_secret' => $clientData['client_secret'], // FIXME: Returning this means we need to patch Nextcloud to accept tokens on calls to - - 'registration_client_uri' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.server.registeredClient", array("clientId" => $clientData['client_id']))), - 'client_id_issued_at' => $clientData['client_id_issued_at'], - 'redirect_uris' => $clientData['redirect_uris'], - ); - $registration = $this->tokenGenerator->respondToRegistration($registration, $this->config->getPrivateKey()); - return (new JSONResponse($registration)); -// ->addHeader('Access-Control-Allow-Origin', $origin) -// ->addHeader('Access-Control-Allow-Methods', 'POST'); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function registeredClient($clientId) { - $clientRegistration = $this->config->getClientRegistration($clientId); - unset($clientRegistration['client_secret']); - return new JSONResponse($clientRegistration); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function jwks() { - $response = new \Laminas\Diactoros\Response(); - $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); - $response = $server->respondToJwksMetadataRequest(); - return $this->respond($response); - } - - private function respond($response) { - $statusCode = $response->getStatusCode(); - $response->getBody()->rewind(); - $headers = $response->getHeaders(); - - $body = json_decode($response->getBody()->getContents()); - if ($statusCode > 399) { - // var_dump($body); - $reason = $response->getReasonPhrase(); - $result = new JSONResponse($reason, $statusCode); - return $result; - } - - if ($body == null) { - $body = 'ok'; - } - $result = new JSONResponse($body); - - foreach ($headers as $header => $values) { - foreach ($values as $value) { - $result->addHeader($header, $value); - } - } - $result->setStatus($statusCode); -// $result->addHeader('Access-Control-Allow-Origin', '*'); - return $result; - } - - private function getClient($clientId) { - $clientRegistration = $this->config->getClientRegistration($clientId); - - if ($clientId && count($clientRegistration)) { - return new \Pdsinterop\Solid\Auth\Config\Client( - $clientId, - $clientRegistration['client_secret'] ?? '', - $clientRegistration['redirect_uris'], - $clientRegistration['client_name'] - ); - } else { - return new \Pdsinterop\Solid\Auth\Config\Client('','',array(),''); - } - } -} diff --git a/solid/lib/Controller/StorageController.php~ b/solid/lib/Controller/StorageController.php~ deleted file mode 100644 index 07541d13..00000000 --- a/solid/lib/Controller/StorageController.php~ +++ /dev/null @@ -1,471 +0,0 @@ -config = new \OCA\Solid\ServerConfig($config, $urlGenerator, $userManager); - $this->rootFolder = $rootFolder; - $this->request = $request; - $this->urlGenerator = $urlGenerator; - $this->session = $session; - - $this->setJtiStorage($connection); - } - - private function getFileSystem() { - // Create the Nextcloud Adapter - $adapter = new \Pdsinterop\Flysystem\Adapter\Nextcloud($this->solidFolder); - $graph = new \EasyRdf\Graph(); - - // Create Formats objects - $formats = new \Pdsinterop\Rdf\Formats(); - - $serverParams = $this->rawRequest->getServerParams(); - $scheme = $serverParams['REQUEST_SCHEME']; - $domain = $serverParams['SERVER_NAME']; - $path = $serverParams['REQUEST_URI']; - $serverUri = "{$scheme}://{$domain}{$path}"; // FIXME: doublecheck that this is the correct url; - - // Create the RDF Adapter - $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( - $adapter, - $graph, - $formats, - $serverUri - ); - - $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); - - $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - - $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); - $filesystem->addPlugin($plugin); - - return $filesystem; - } - - private function getUserProfile($userId) { - return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute("solid.profile.handleGet", array("userId" => $userId, "path" => "/card"))) . "#me"; - } - - private function generateDefaultAcl($userId) { - $defaultAcl = <<< EOF -# Root ACL resource for the user account -@prefix acl: . -@prefix foaf: . - -# The homepage is readable by the public -<#public> - a acl:Authorization; - acl:agentClass foaf:Agent; - acl:accessTo <./>; - acl:mode acl:Read. - -# The owner has full access to every resource in their pod. -# Other agents have no access rights, -# unless specifically authorized in other .acl resources. -<#owner> - a acl:Authorization; - acl:agent <{user-profile-uri}>; - # Set the access to the root storage folder itself - acl:accessTo <./>; - # All resources will inherit this authorization, by default - acl:default <./>; - # The owner has all of the access modes allowed - acl:mode - acl:Read, acl:Write, acl:Control. -EOF; - - $profileUri = $this->getUserProfile($userId); - $defaultAcl = str_replace("{user-profile-uri}", $profileUri, $defaultAcl); - return $defaultAcl; - } - - private function generatePublicAppendAcl($userId) { - $publicAppendAcl = <<< EOF -# Inbox ACL resource for the user account -@prefix acl: . -@prefix foaf: . - -<#public> - a acl:Authorization; - acl:agentClass foaf:Agent; - acl:accessTo <./>; - acl:default <./>; - acl:mode - acl:Append. - -<#owner> - a acl:Authorization; - acl:agent <{user-profile-uri}>; - # Set the access to the root storage folder itself - acl:accessTo <./>; - # All resources will inherit this authorization, by default - acl:default <./>; - # The owner has all of the access modes allowed - acl:mode - acl:Read, acl:Write, acl:Control. -EOF; - - $profileUri = $this->getUserProfile($userId); - $publicAppendAcl = str_replace("{user-profile-uri}", $profileUri, $publicAppendAcl); - return $publicAppendAcl; - } - - private function generatePublicReadAcl($userId) { - $publicReadAcl = <<< EOF -# Inbox ACL resource for the user account -@prefix acl: . -@prefix foaf: . - -<#public> - a acl:Authorization; - acl:agentClass foaf:Agent; - acl:accessTo <./>; - acl:default <./>; - acl:mode - acl:Read. - -<#owner> - a acl:Authorization; - acl:agent <{user-profile-uri}>; - # Set the access to the root storage folder itself - acl:accessTo <./>; - # All resources will inherit this authorization, by default - acl:default <./>; - # The owner has all of the access modes allowed - acl:mode - acl:Read, acl:Write, acl:Control. -EOF; - - $profileUri = $this->getUserProfile($userId); - $publicReadAcl = str_replace("{user-profile-uri}", $profileUri, $publicReadAcl); - return $publicReadAcl; - } - - private function generateDefaultPublicTypeIndex() { - $publicTypeIndex = <<< EOF -# Public type index -@prefix : <#>. -@prefix solid: . - -<> - a solid:ListedDocument, solid:TypeIndex. -EOF; - - return $publicTypeIndex; - } - - private function generateDefaultPrivateTypeIndex() { - $privateTypeIndex = <<< EOF -# Private type index -@prefix : <#>. -@prefix solid: . - -<> - a solid:UnlistedDocument, solid:TypeIndex. -EOF; - - return $privateTypeIndex; - } - private function generateDefaultPreferences($userId) { - $preferences = <<< EOF -# Preferences -@prefix : <#>. -@prefix sp: . -@prefix dct: . -@prefix profile: <{user-profile-uri}>. -@prefix solid: . - -<> - a sp:ConfigurationFile; - dct:title "Preferences file". - -profile:me - a solid:Developer; - solid:privateTypeIndex ; - solid:publicTypeIndex . -EOF; - - $profileUri = $this->getUserProfile($userId); - $preferences = str_replace("{user-profile-uri}", $profileUri, $preferences); - return $preferences; - } - private function initializeStorage($userId) { - $this->userFolder = $this->rootFolder->getUserFolder($userId); - if (!$this->userFolder->nodeExists("solid")) { - $this->userFolder->newFolder("solid"); // Create the Solid directory for storage if it doesn't exist. - } - $this->solidFolder = $this->userFolder->get("solid"); - - $this->filesystem = $this->getFileSystem(); - - // Make sure the root folder has an acl file, as is required by the spec; - // Generate a default file granting the owner full access if there is nothing there. - if (!$this->filesystem->has("/.acl")) { - $defaultAcl = $this->generateDefaultAcl($userId); - $this->filesystem->write("/.acl", $defaultAcl); - } - - // Generate default folders and ACLs: - if (!$this->filesystem->has("/inbox")) { - $this->filesystem->createDir("/inbox"); - } - if (!$this->filesystem->has("/inbox/.acl")) { - $inboxAcl = $this->generatePublicAppendAcl($userId); - $this->filesystem->write("/inbox/.acl", $inboxAcl); - } - if (!$this->filesystem->has("/settings")) { - $this->filesystem->createDir("/settings"); - } - if (!$this->filesystem->has("/settings/privateTypeIndex.ttl")) { - $privateTypeIndex = $this->generateDefaultPrivateTypeIndex(); - $this->filesystem->write("/settings/privateTypeIndex.ttl", $privateTypeIndex); - } - if (!$this->filesystem->has("/settings/publicTypeIndex.ttl")) { - $publicTypeIndex = $this->generateDefaultPublicTypeIndex(); - $this->filesystem->write("/settings/publicTypeIndex.ttl", $publicTypeIndex); - } - if (!$this->filesystem->has("/settings/preferences.ttl")) { - $preferences = $this->generateDefaultPreferences($userId); - $this->filesystem->write("/settings/preferences.ttl", $preferences); - } - if (!$this->filesystem->has("/public")) { - $this->filesystem->createDir("/public"); - } - if (!$this->filesystem->has("/public/.acl")) { - $publicAcl = $this->generatePublicReadAcl($userId); - $this->filesystem->write("/public/.acl", $publicAcl); - } - if (!$this->filesystem->has("/private")) { - $this->filesystem->createDir("/private"); - } - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleRequest($userId, $path) { - $this->rawRequest = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); - $this->response = new \Laminas\Diactoros\Response(); - - $this->initializeStorage($userId); - - $this->resourceServer = new ResourceServer($this->filesystem, $this->response); - $this->WAC = new WAC($this->filesystem); - - $request = $this->rawRequest; - $baseUrl = $this->getStorageUrl($userId); - $this->resourceServer->setBaseUrl($baseUrl); - $this->WAC->setBaseUrl($baseUrl); - - $notifications = new SolidNotifications(); - $this->resourceServer->setNotifications($notifications); - - $dpop = $this->getDpop(); - - $error = false; - try { - $webId = $dpop->getWebId($request); - } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { - $error = $e; - } - - if (!isset($webId)) { - $bearer = $this->getBearer(); - try { - $webId = $bearer->getWebId($request); - } catch(\Pdsinterop\Solid\Auth\Exception\Exception $e) { - $error = $e; - } - } - - if (!isset($webId)) { - $response = $this->resourceServer->getResponse() - ->withStatus(Http::STATUS_CONFLICT, "Invalid token"); - return $this->respond($response); - } - - $origin = $request->getHeaderLine("Origin"); - $allowedClients = $this->config->getAllowedClients($userId); - $allowedOrigins = array(); - foreach ($allowedClients as $clientId) { - $clientRegistration = $this->config->getClientRegistration($clientId); - if (isset($clientRegistration['client_name'])) { - $allowedOrigins[] = $clientRegistration['client_name']; - } - if (isset($clientRegistration['origin'])) { - $allowedOrigins[] = $clientRegistration['origin']; - } - } - if (!$this->WAC->isAllowed($request, $webId, $origin, $allowedOrigins)) { - $response = $this->resourceServer->getResponse() - ->withStatus(403, "Access denied"); - return $this->respond($response); - } - $response = $this->resourceServer->respondToRequest($request); - $response = $this->WAC->addWACHeaders($request, $response, $webId); - return $this->respond($response); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleGet($userId, $path) { - return $this->handleRequest($userId, $path); - } - - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePost($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePut() { // $userId, $path) { - // FIXME: Adding the correct variables in the function name will make nextcloud - // throw an error about accessing put twice, so we will find out the userId and path from $_SERVER instead; - -<<<<<<< HEAD - // because we got here, the request uri should look like: - // - if we have user subdomains enabled: - // /index.php/apps/solid/storage{path} - // and otherwise: - // index.php/apps/solid/~{userId}/storage{path} -======= - // because we got here, the request uri should look like: - // /index.php/apps/solid/~{userId}/storage{path} - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^storage/", "", $path); ->>>>>>> 3100599 (replace @ with ~ in urls) - - // In the first case, we'll get the username from the SERVER_NAME. In the latter, it will come from the URL; - if ($this->config->getUserSubDomainsEnabled()) { - $pathInfo = explode("storage/", $_SERVER['REQUEST_URI']); - $path = $pathInfo[1]; - $userId = explode(".", $_SERVER['SERVER_NAME'])[0]; - } else { - $pathInfo = explode("~", $_SERVER['REQUEST_URI']); - $pathInfo = explode("/", $pathInfo[1], 2); - $userId = $pathInfo[0]; - $path = $pathInfo[1]; - $path = preg_replace("/^storage/", "", $path); - } - - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleDelete($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handleHead($userId, $path) { - return $this->handleRequest($userId, $path); - } - /** - * @PublicPage - * @NoAdminRequired - * @NoCSRFRequired - */ - public function handlePatch($userId, $path) { - return $this->handleRequest($userId, $path); - } - - private function respond($response) { - $statusCode = $response->getStatusCode(); - $response->getBody()->rewind(); - $headers = $response->getHeaders(); - - $body = $response->getBody()->getContents(); - - $result = new PlainResponse($body); - - foreach ($headers as $header => $values) { - $result->addHeader($header, implode(", ", $values)); - } - -// $origin = $_SERVER['HTTP_ORIGIN']; -// $result->addHeader('Access-Control-Allow-Credentials', 'true'); -// $result->addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); -// $result->addHeader('Access-Control-Allow-Origin', $origin); - - $policy = new EmptyContentSecurityPolicy(); - $policy->addAllowedStyleDomain("*"); - $policy->addAllowedStyleDomain("data:"); - $policy->addAllowedScriptDomain("*"); - $policy->addAllowedImageDomain("*"); - $policy->addAllowedFontDomain("*"); - $policy->addAllowedConnectDomain("*"); - $policy->allowInlineStyle(true); - // $policy->allowInlineScript(true); - removed, this function no longer exists in NC28 - $policy->allowEvalScript(true); - $result->setContentSecurityPolicy($policy); - - $result->setStatus($statusCode); - return $result; - } -}