├── config └── placeholder.txt ├── .dockerignore ├── .travis.yml ├── .gitignore ├── web ├── .htaccess └── index.php ├── tests └── fixtures │ ├── storage │ └── settings │ │ ├── privateTypeIndex.ttl │ │ ├── publicTypeIndex.ttl │ │ ├── preferencesFile.ttl │ │ └── .acl │ └── foaf.rdf ├── Gemfile ├── env-vars-for-third-party.list ├── CODE_OF_CONDUCT.md ├── src ├── Controller │ ├── AddSlashToPathController.php │ ├── CorsController.php │ ├── JwksController.php │ ├── OpenidController.php │ ├── ApprovalController.php │ ├── HttpToHttpsController.php │ ├── HandleApprovalController.php │ ├── StorageController.php │ ├── RegisterController.php │ ├── AbstractController.php │ ├── TokenController.php │ ├── AbstractRedirectController.php │ ├── Profile │ │ ├── ProfileController.php │ │ └── CardController.php │ ├── LoginController.php │ ├── HelloWorldController.php │ ├── AuthorizeController.php │ ├── ResourceController.php │ └── ServerController.php ├── Template │ ├── approval.html │ ├── login.html │ ├── default.html │ └── card.html ├── Traits │ ├── HasResponseTrait.php │ ├── HasFilesystemTrait.php │ └── HasTemplateTrait.php ├── Service │ ├── RouterService.php │ └── ContainerService.php ├── ExceptionResponse.php └── ServerConfig.php ├── bin └── serve-docker-dev.sh ├── CONTRIBUTING.md ├── env-vars-for-test-image.list ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── docker-compose.yml ├── site.conf ├── _config.yml ├── Dockerfile ├── docs └── spec-compliance │ └── server.md ├── composer.json ├── run-solid-test-suite.sh ├── CHANGELOG.md ├── .github_changelog_generator └── README.md /config/placeholder.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | run-solid-test-suite.sh 3 | .git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | script: 4 | - bash run-solid-test-suite.sh 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories to ignore 2 | /build 3 | /vendor 4 | 5 | # Files to ignore 6 | /.env 7 | /phpunit.xml -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteCond %{REQUEST_FILENAME} !-d 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule . index.php [L] 5 | -------------------------------------------------------------------------------- /tests/fixtures/storage/settings/privateTypeIndex.ttl: -------------------------------------------------------------------------------- 1 | @prefix solid: . 2 | <> 3 | a solid:TypeIndex ; 4 | a solid:UnlistedDocument. 5 | -------------------------------------------------------------------------------- /tests/fixtures/storage/settings/publicTypeIndex.ttl: -------------------------------------------------------------------------------- 1 | @prefix : <#>. 2 | @prefix solid: . 3 | @prefix schem: . 4 | 5 | <> a solid:ListedDocument, solid:TypeIndex. -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # gem "rails" 8 | 9 | gem "jekyll", "~> 3.8" 10 | gem "github-pages" 11 | -------------------------------------------------------------------------------- /env-vars-for-third-party.list: -------------------------------------------------------------------------------- 1 | ALICE_WEBID=https://thirdparty/profile/card#me 2 | SERVER_ROOT_ESCAPED=https:\/\/thirdparty 3 | SERVER_ROOT=https://thirdparty 4 | STORAGE_ROOT=https://thirdparty/storage 5 | USERNAME=alice 6 | PASSWORD=alice123 7 | PUBSUB_URL=http://pubsub:8080 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | All PDS Interop projects adhere to [the Code Manifesto](http://codemanifesto.com) 4 | as its guidelines for contributor and community interactions. 5 | 6 | For full details visit: [https://pdsinterop.org/code-of-conduct/](https://pdsinterop.org/code-of-conduct/) 7 | -------------------------------------------------------------------------------- /src/Controller/AddSlashToPathController.php: -------------------------------------------------------------------------------- 1 | getPath() . '/' . $this->getQuery(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Template/approval.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Requesting approval

5 |
6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /bin/serve-docker-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run a PHP development server in docker 4 | 5 | docker run \ 6 | -i \ 7 | --expose 443 \ 8 | --name server \ 9 | --network host \ 10 | --rm \ 11 | --volume "${PWD}:/app" \ 12 | "${DOCKER_IMAGE:=php-solid-server}" 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the PHP standalone Solid Server 2 | 3 | Thank you for your interest in contributing! 4 | 5 | All PDS Interop projects are open source and community-friendly. 6 | 7 | Please visit [https://pdsinterop.org/contributing/](https://pdsinterop.org/contributing/) for details about: 8 | 9 | - Our [Code of Conduct](https://pdsinterop.org/code-of-conduct/) 10 | - How to report issues 11 | - How to make code changes 12 | - How to send a merge request 13 | -------------------------------------------------------------------------------- /src/Controller/CorsController.php: -------------------------------------------------------------------------------- 1 | getResponse()->withHeader("Access-Control-Allow-Headers", "*"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fixtures/storage/settings/preferencesFile.ttl: -------------------------------------------------------------------------------- 1 | @prefix : <#>. 2 | @prefix dct: . 3 | @prefix c: . 4 | @prefix terms: . 5 | @prefix n0: . 6 | @prefix sp: . 7 | 8 | c:me 9 | a terms:Developer; 10 | terms:privateTypeIndex ; 11 | terms:publicTypeIndex ; 12 | <> a sp:ConfigurationFile; dct:title "Preferences file". 13 | -------------------------------------------------------------------------------- /env-vars-for-test-image.list: -------------------------------------------------------------------------------- 1 | ALICE_WEBID=https://server/profile/card#me 2 | WEBID_ALICE=https://server/profile/card#me 3 | WEBID_BOB=https://thirdparty/profile/card#me 4 | SERVER_ROOT_ESCAPED=https:\/\/server 5 | SERVER_ROOT=https://server 6 | OIDC_ISSUER_ALICE=https://server 7 | OIDC_ISSUER_BOB=https://thirdparty 8 | STORAGE_ROOT=https://server/storage 9 | STORAGE_ROOT_ALICE=https://server/storage 10 | STORAGE_ROOT_BOB=https://thirdparty/storage 11 | USERNAME=alice 12 | PASSWORD=alice123 13 | PUBSUB_URL=http://pubsub:8080 14 | SKIP_CONC=1 15 | -------------------------------------------------------------------------------- /src/Controller/JwksController.php: -------------------------------------------------------------------------------- 1 | getResponse(); 13 | $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); 14 | return $server->respondToJwksMetadataRequest(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Controller/OpenidController.php: -------------------------------------------------------------------------------- 1 | getResponse(); 13 | $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); 14 | return $server->respondToOpenIdMetadataRequest(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Template/login.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Please login

5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /src/Controller/ApprovalController.php: -------------------------------------------------------------------------------- 1 | getQueryParams()['returnUrl']; 14 | 15 | return $this->createTemplateResponse('approval.html', [ 16 | 'clientId' => $clientId, 17 | 'returnUrl' => $returnUrl, 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | # Run the Solid test-suite 24 | - run: bash ./run-solid-test-suite.sh 25 | -------------------------------------------------------------------------------- /src/Template/default.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |

This will be replaced with content from other templates

18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Traits/HasResponseTrait.php: -------------------------------------------------------------------------------- 1 | response; 19 | } 20 | 21 | /** 22 | * @param ResponseInterface $response 23 | */ 24 | public function setResponse(ResponseInterface $response) : void 25 | { 26 | $this->response = $response; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/fixtures/storage/settings/.acl: -------------------------------------------------------------------------------- 1 | # ACL resource for the /settings/ container 2 | @prefix acl: . 3 | @prefix foaf: . 4 | 5 | <#public> 6 | a acl:Authorization; 7 | acl:agentClass foaf:Agent; 8 | acl:accessTo <./publicTypeIndex.ttl>; 9 | acl:mode 10 | acl:Read. 11 | 12 | <#owner> 13 | a acl:Authorization; 14 | 15 | acl:agent 16 | ; 17 | 18 | # Set the access to the root storage folder itself 19 | acl:accessTo <./>; 20 | 21 | # All settings resources will be private, by default, unless overridden 22 | acl:default <./>; 23 | 24 | # The owner has all of the access modes allowed 25 | acl:mode 26 | acl:Read, acl:Write, acl:Control. 27 | 28 | # Private, no public access modes 29 | -------------------------------------------------------------------------------- /src/Traits/HasFilesystemTrait.php: -------------------------------------------------------------------------------- 1 | filesystem; 22 | } 23 | 24 | /** 25 | * @param FilesystemInterface $filesystem 26 | */ 27 | public function setFilesystem(FilesystemInterface $filesystem) : void 28 | { 29 | $this->filesystem = $filesystem; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Controller/HttpToHttpsController.php: -------------------------------------------------------------------------------- 1 | getServerParams(); 13 | 14 | if (isset($serverParams['HTTP_HOST']) === false) { 15 | $message = 'Could not determine host name.' . 16 | 'The server\'s HTTP_HOST variable has not been set!'; 17 | 18 | $response = $this->createTextResponse($message, 500); 19 | } else { 20 | $url = 'https://' . $serverParams['HTTP_HOST'] . $request->getRequestTarget(); 21 | 22 | $response = $this->createRedirectResponse($url, 301); 23 | } 24 | 25 | return $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Controller/HandleApprovalController.php: -------------------------------------------------------------------------------- 1 | getParsedBody()['returnUrl']; 14 | $approval = $request->getParsedBody()['approval']; 15 | 16 | if ($approval == "allow") { 17 | $this->config->addAllowedClient($this->userId, $clientId); 18 | } else { 19 | $this->config->removeAllowedClient($this->userId, $clientId); 20 | } 21 | 22 | $response = $this->getResponse(); 23 | $response = $response->withHeader("Location", $returnUrl); 24 | $response = $response->withStatus("302", "ok"); 25 | return $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Controller/StorageController.php: -------------------------------------------------------------------------------- 1 | . 14 | @prefix inbox: <>. 15 | @prefix ldp: . 16 | @prefix terms: . 17 | @prefix XML: . 18 | @prefix st: . 19 | 20 | inbox: 21 | a ldp:BasicContainer, ldp:Container, ldp:Resource; 22 | terms:modified "2019-12-20T14:52:54Z"^^XML:dateTime; 23 | st:mtime 1576853574.389; 24 | st:size 4096. 25 | EOF; 26 | 27 | return $this->createTextResponse($body)->withHeader("Content-type", "text/turtle"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 PDS Interop 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | php-solid-server: 4 | build: 5 | context: . 6 | container_name: php-solid-server 7 | environment: 8 | # During development it can be useful to set the ENVIRONMENT to "development" 9 | # in order to see more details about the errors. 10 | # ENVIRONMENT: development 11 | USERNAME: alice 12 | PASSWORD: alice123 13 | # To change the root from https://localhost to something sensible , set SERVER_ROOT, for example: 14 | # SERVER_ROOT: https://nextcloud.local 15 | # to run in HTTP mode, set PROXY_MODE 16 | # PROXY_MODE: true 17 | PUBSUB_URL: http://pubsub:8080 18 | ports: 19 | - 80:80 20 | - 443:443 21 | volumes: 22 | - .:/app/ 23 | # @TODO: The storage directory should be mounted separately 24 | # as it really _should_ live outside the code directory 25 | 26 | pubsub: 27 | build: 28 | context: https://github.com/pdsinterop/php-solid-pubsub-server.git#main 29 | ports: 30 | - 8080:8080 31 | -------------------------------------------------------------------------------- /site.conf: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot "/app/web/" 3 | ErrorLog ${APACHE_LOG_DIR}/error.log 4 | CustomLog ${APACHE_LOG_DIR}/access.log combined 5 | 6 | SSLEngine on 7 | SSLCertificateFile "/tls/server.cert" 8 | SSLCertificateKeyFile "/tls/server.key" 9 | 10 | 11 | SSLOptions +StdEnvVars 12 | 13 | 14 | 15 | Options Indexes FollowSymLinks 16 | AllowOverride all 17 | Require all granted 18 | 19 | DirectoryIndex index.php 20 | 21 | 22 | RewriteEngine On 23 | RewriteCond %{HTTP:Authorization} ^(.*) 24 | RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] 25 | 26 | SetEnv AccessControlAllowOrigin="*" 27 | SetEnvIf Origin "^(.*)$" AccessControlAllowOrigin=$0 28 | Header add Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin 29 | Header add Access-Control-Allow-Credentials true 30 | Header add Access-Control-Allow-Headers "authorization, content-type, dpop" 31 | Header add Access-Control-Allow-Methods "GET, PUT, POST, OPTIONS, DELETE, PATCH" 32 | Header add Accept-Patch: application/sparql-update 33 | Header add Access-Control-Expose-Headers: "Accept-Patch" 34 | 35 | -------------------------------------------------------------------------------- /src/Controller/RegisterController.php: -------------------------------------------------------------------------------- 1 | withStatus(400, "Missing redirect URIs"); 17 | } 18 | $clientData['client_id_issued_at'] = time(); 19 | $parsedOrigin = parse_url($clientData['redirect_uris'][0]); 20 | $origin = $parsedOrigin['host']; 21 | 22 | $clientId = $this->config->saveClientRegistration($origin, $clientData); 23 | 24 | // FIXME: properly generate this url; 25 | $baseUrl = $this->baseUrl; 26 | $clientUrl = $baseUrl . "/clients/$clientId"; 27 | 28 | $registration = array( 29 | 'client_id' => $clientId, 30 | 'registration_client_uri' => $clientUrl, 31 | 'client_id_issued_at' => $clientData['client_id_issued_at'], 32 | 'redirect_uris' => $clientData['redirect_uris'], 33 | ); 34 | 35 | $registration = $this->tokenGenerator->respondToRegistration($registration, $this->config->getPrivateKey()); 36 | return new JsonResponse($registration); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | getResponse() 24 | ->withHeader('location', $url) 25 | ->withStatus($status) 26 | ; 27 | } 28 | 29 | final public function createTemplateResponse(string $template, array $context = []) : ResponseInterface 30 | { 31 | $response = $this->buildTemplate($template, $context); 32 | 33 | return $this->createTextResponse($response); 34 | } 35 | 36 | final public function createTextResponse(string $message, int $status = 200) : ResponseInterface 37 | { 38 | $response = $this->getResponse(); 39 | 40 | $body = $response->getBody(); 41 | 42 | $body->write($message); 43 | 44 | return $response->withBody($body)->withStatus($status); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | baseurl: /php-solid-server 3 | description: Standalone Solid Server written in PHP by PDS Interop 4 | repository: pdsinterop/php-solid-server 5 | title: Standalone PHP Solid Server 6 | url: https://pdsinterop.org 7 | 8 | permalink: pretty 9 | remote_theme: Potherca/extend-the-docs@v0.3.1 10 | 11 | exclude: 12 | - "bin/" 13 | - "src/" 14 | - "tests/" 15 | - "vendor/" 16 | - "Gemfile" 17 | - "*.json" 18 | - "*.lock" 19 | 20 | plugins: 21 | - github-pages 22 | - jekyll-github-metadata 23 | - jekyll-remote-theme 24 | - jekyll-seo-tag 25 | 26 | # Extend the Docs settings (see https://pother.ca/extend-the-docs/) 27 | nav: 28 | cross_repository: 29 | exclude: 30 | - jekyll-theme 31 | - pdsinterop.github.io 32 | show_archived: true 33 | show_homepage: false 34 | exclude: 35 | - / 36 | - /404.html 37 | favicon_ico : /favicon.ico 38 | main_title: 39 | link: '/' 40 | recurse: true 41 | 42 | # Just the Docs settings (see https://pmarsceill.github.io/just-the-docs/docs/configuration/) 43 | aux_links: 44 | "PDS Interop on GitHub": 45 | - https://github.com/pdsinterop 46 | footer_content: '

Copyright © 2020-2021 PDS Interop. Distributed under a MIT license.

' 47 | gh_edit_link: true 48 | gh_edit_repository: https://github.com/pdsinterop/php-solid-server/ 49 | logo: https://avatars3.githubusercontent.com/u/65920341 50 | search_enabled: true 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.3-apache 2 | 3 | # ============================================================================== 4 | # Set up the machine 5 | # ------------------------------------------------------------------------------ 6 | COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer 7 | 8 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 9 | 10 | RUN export DEBIAN_FRONTEND=noninteractive \ 11 | && apt-get update && \ 12 | apt-get install -y --no-install-recommends \ 13 | git \ 14 | libzip-dev \ 15 | zlib1g-dev \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | RUN mkdir /tls && openssl req -new -x509 -days 365 -nodes \ 19 | -out /tls/server.cert \ 20 | -keyout /tls/server.key \ 21 | -subj "/C=NL/ST=Overijssel/L=Enschede/O=PDS Interop/OU=IT/CN=pdsinterop.org" 22 | 23 | RUN docker-php-ext-install \ 24 | bcmath \ 25 | mbstring \ 26 | mysqli \ 27 | pdo \ 28 | pdo_mysql \ 29 | zip 30 | 31 | RUN a2enmod headers rewrite ssl 32 | 33 | COPY site.conf /etc/apache2/sites-enabled/site.conf 34 | 35 | WORKDIR /app 36 | 37 | EXPOSE 443 38 | # ============================================================================== 39 | 40 | 41 | # ============================================================================== 42 | # Add the source code 43 | # ------------------------------------------------------------------------------ 44 | ARG PROJECT_PATH 45 | RUN : "${PROJECT_PATH:=$PWD}" 46 | 47 | COPY "${PROJECT_PATH}" /app/ 48 | 49 | RUN composer install --no-dev --prefer-dist 50 | RUN chown -R www-data:www-data /app 51 | # ============================================================================== 52 | -------------------------------------------------------------------------------- /src/Template/card.html: -------------------------------------------------------------------------------- 1 | 2 | 24 |
25 |

Profile in various RDF formats

26 |
27 | 34 |
35 | 36 |
37 | 38 | 39 |
40 |

RDF Format

41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /src/Controller/TokenController.php: -------------------------------------------------------------------------------- 1 | getParsedBody()['code']; 16 | $clientId = $request->getParsedBody()['client_id']; 17 | $DPop = new DPop(); 18 | $dpop = $request->getServerParams()['HTTP_DPOP']; 19 | try { 20 | $dpopKey = $DPop->getDPopKey($dpop, $request); 21 | } catch(\Exception $e) { 22 | return $this->getResponse()->withStatus(409, "Invalid token"); 23 | } 24 | 25 | $server = new \Pdsinterop\Solid\Auth\Server( 26 | $this->authServerFactory, 27 | $this->authServerConfig, 28 | new \Laminas\Diactoros\Response() 29 | ); 30 | 31 | $response = $server->respondToAccessTokenRequest($request); 32 | 33 | // FIXME: not sure if decoding this here is the way to go. 34 | // FIXME: because this is a public page, the nonce from the session is not available here. 35 | $codeInfo = $this->tokenGenerator->getCodeInfo($code); 36 | 37 | return $this->tokenGenerator->addIdTokenToResponse($response, 38 | $clientId, 39 | $codeInfo['user_id'], 40 | $_SESSION['nonce'], 41 | $this->config->getPrivateKey(), 42 | $dpopKey 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Controller/AbstractRedirectController.php: -------------------------------------------------------------------------------- 1 | request->getRequestTarget(); 23 | 24 | [$path, $query] = explode('?', $target); 25 | 26 | return $path; 27 | } 28 | 29 | /** @return string */ 30 | final public function getQuery() : string 31 | { 32 | $target = $this->request->getRequestTarget(); 33 | 34 | [$url, $query] = explode('?', $target); 35 | 36 | if (is_string($query)) { 37 | $query .= '?' . $query; 38 | } 39 | 40 | 41 | return (string) $query; 42 | } 43 | 44 | abstract public function getTargetUrl(): string; 45 | 46 | //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 47 | 48 | final public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface 49 | { 50 | $this->request = $request; 51 | $this->args = $args; 52 | 53 | return $this->createRedirectResponse($this->getTargetUrl()); 54 | } 55 | 56 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 57 | 58 | } 59 | -------------------------------------------------------------------------------- /docs/spec-compliance/server.md: -------------------------------------------------------------------------------- 1 | # Server 2 | 3 | This document describes what the Solid specification demands from a server in 4 | general. Implementation details have been added describing if and how compliance 5 | has been reached. 6 | 7 | ## Spec compliance 8 | 9 | - [x] _All http URIs MUST redirect to their https counterparts using a response with a 301 status code and a Location header._
10 | ![][ready] Implemented through the `Pdsinterop\Solid\Controller\HttpToHttpsController`. 11 | 12 | - [ ] _It SHOULD additionally implement the server part of HTTP/1.1 Caching to improve performance_
13 | ![][maybe] As caching can be added "in-front" of the server this is deemed low-priority. 14 | 15 | - [ ] _When a client does not provide valid credentials when requesting a resource that requires it, the data pod MUST send a response with a 401 status code (unless 404 is preferred for security reasons)._
16 | ![][later] This will need to be implemented as part of the OAuth, ACL, and protected documents parts. 17 | 18 | - [ ] _A Solid server MUST reject PUT, POST and PATCH requests without the Content-Type header with a status code of 400._
19 | ![][todo] This should be added in a similar fashion as the HTTPtheHTTPS mechanism. No need to continue routing if this criteria is not met. 20 | 21 | - [x] _Paths ending with a slash denote a container resource. the server MAY respond to requests for the latter URI with a 301 redirect to the former._
22 | ![][ready] Implemented through the `Pdsinterop\Solid\Controller\AddSlashToPathController` 23 | 24 | [later]: https://img.shields.io/badge/resolution-later-important.svg 25 | [maybe]: https://img.shields.io/badge/resolution-maybe%20later-yellow.svg 26 | [ready]: https://img.shields.io/badge/resolution-done-success.svg 27 | [todo]: https://img.shields.io/badge/resolution-todo-critical.svg 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "psr-4" :{ 4 | "Pdsinterop\\Solid\\": "src/" 5 | } 6 | }, 7 | "config": { 8 | "bin-dir": "./bin", 9 | "sort-packages": true 10 | }, 11 | "description": "Standalone Solid Server written in PHP by PDS Interop.", 12 | "license": "MIT", 13 | "name": "pdsinterop/solid-server", 14 | "require": { 15 | "php": "^7.3", 16 | "ext-dom": "*", 17 | "ext-json": "*", 18 | "ext-mbstring": "*", 19 | "ext-openssl": "*", 20 | "codercat/jwk-to-pem": "^1.1", 21 | "defuse/php-encryption": "^2.3", 22 | "laminas/laminas-diactoros": " ^2.8", 23 | "laminas/laminas-httphandlerrunner": "^1.5", 24 | "lcobucci/jwt": "3.3.3", 25 | "league/container": "^3.4", 26 | "league/flysystem": "^1.1", 27 | "league/oauth2-server": "^8.1", 28 | "league/route": "^4.5", 29 | "pdsinterop/flysystem-rdf": "^0.3", 30 | "pdsinterop/solid-auth": "^0.6", 31 | "pdsinterop/solid-crud": "^0.3", 32 | "php-http/httplug": "^2.2", 33 | "phptal/phptal": "^1.5" 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "^8.5 | ^9.5" 37 | }, 38 | "scripts": { 39 | "lint":"", 40 | "serve-dev":"USERNAME=alice PASSWORD=alice123 ENVIRONMENT=development SERVER_ROOT=\"http://${HOST:-localhost}:${PORT:-8080}\" php -S \"${HOST:-localhost}:${PORT:-8080}\" -t web/ web/index.php", 41 | "serve-dev-docker":"bash ./bin/serve-docker-dev.sh", 42 | "test":"phpunit" 43 | }, 44 | "scripts-descriptions": { 45 | "serve-dev": "Run the application with the internal PHP development server", 46 | "serve-dev-docker": "Run the application with the docker image provided by the TestSuite repo.", 47 | "test": "Run unit-test with PHPUnit" 48 | }, 49 | "type": "project" 50 | } 51 | -------------------------------------------------------------------------------- /src/Controller/Profile/ProfileController.php: -------------------------------------------------------------------------------- 1 | request = $request; 26 | 27 | $filesystem = $this->getFilesystem(); 28 | 29 | $formats = Format::keys(); 30 | $contents = $this->fetchFileContents($filesystem, $formats); 31 | 32 | return $this->createTemplateResponse('card.html', [ 33 | 'files' => $contents, 34 | 'formats' => $formats, 35 | ]); 36 | } 37 | 38 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 39 | 40 | /** 41 | * @param FilesystemInterface $filesystem 42 | * @param array string[] 43 | * 44 | * @return string[] 45 | */ 46 | private function fetchFileContents(FilesystemInterface $filesystem, array $formats) : array 47 | { 48 | $contents = []; 49 | 50 | $serverParams = $this->request->getServerParams(); 51 | $url = $serverParams["REQUEST_URI"] ?? ''; 52 | 53 | array_walk($formats, static function ($format, $index) use (&$contents, $filesystem, $url) { 54 | /** @noinspection PhpUndefinedMethodInspection */ // Method `readRdf` is defined by plugin 55 | $contents[$index] = $filesystem->readRdf('/foaf.rdf', $format, $url); 56 | }); 57 | 58 | return $contents; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Controller/LoginController.php: -------------------------------------------------------------------------------- 1 | getParsedBody(); 13 | $response = $this->getResponse(); 14 | 15 | if ($request->getMethod() === 'POST') { 16 | if (isset($_SESSION['userid'])) { 17 | $user = $_SESSION['userid']; 18 | 19 | if (isset($request->getQueryParams()['returnUrl'])) { 20 | return $response 21 | ->withHeader("Location", $request->getQueryParams()['returnUrl']) 22 | ->withStatus(302) 23 | ; 24 | } 25 | 26 | $response->getBody()->write("

Already logged in as $user

"); 27 | } elseif ($postBody['username'] && $postBody['password']) { 28 | $user = $postBody['username']; 29 | $password = $postBody['password']; 30 | 31 | if ( 32 | ($user === $_ENV['USERNAME'] && $password === $_ENV['PASSWORD']) 33 | || ($user === $_SERVER['USERNAME'] && $password === $_SERVER['PASSWORD']) 34 | ) { 35 | $_SESSION['userid'] = $user; 36 | 37 | if (isset($request->getQueryParams()['returnUrl'])) { 38 | return $response 39 | ->withHeader("Location", $request->getQueryParams()['returnUrl']) 40 | ->withStatus(302) 41 | ; 42 | } 43 | 44 | $response->getBody()->write("

Welcome $user

\n"); 45 | } else { 46 | $response->getBody()->write("

Login as $user failed

\n"); 47 | } 48 | } else { 49 | $response->getBody()->write("

Login failed

\n"); 50 | } 51 | } else { 52 | return $this->createTemplateResponse('login.html'); 53 | } 54 | 55 | return $response; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Traits/HasTemplateTrait.php: -------------------------------------------------------------------------------- 1 | template; 19 | } 20 | 21 | public function setTemplate(PHPTAL $template) : void 22 | { 23 | $this->template = $template; 24 | } 25 | 26 | public function buildTemplate(string $template, array $context) : string 27 | { 28 | $engine = $this->getTemplate(); 29 | 30 | $templateExists = $this->isTemplate($template, $engine); 31 | 32 | if ($templateExists === true) { 33 | $engine->setTemplate($template); 34 | } else { 35 | $template = $this->createTemplateFromString($template); 36 | 37 | $engine->setSource($template); 38 | } 39 | 40 | $engine = $this->setContextOnTemplate($context, $engine); 41 | 42 | return $engine->execute(); 43 | } 44 | 45 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 46 | 47 | private function createTemplateFromString(string $template) : string 48 | { 49 | return <<<"TAL" 50 | 51 |
52 | {$template} 53 |
54 |
55 | TAL; 56 | } 57 | 58 | private function isTemplate(string $template, \PHPTAL $engine) : bool 59 | { 60 | $templateExists = array_map(static function ($templateRepository) use ($template) { 61 | return file_exists($templateRepository . '/' . ltrim($template, '/')); 62 | }, $engine->getTemplateRepositories()); 63 | 64 | return array_sum($templateExists) > 0; 65 | } 66 | 67 | private function setContextOnTemplate(array $context, \PHPTAL $engine) : \PHPTAL 68 | { 69 | array_walk($context, static function ($value, $name) use (&$engine) { 70 | $engine->set($name, $value); 71 | }); 72 | 73 | return $engine; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Controller/HelloWorldController.php: -------------------------------------------------------------------------------- 1 | Hello, World! 15 |

The following main URL are available on this server:

16 | 58 | 59 |
60 |

Footnotes

61 |
    62 |
  1. Also available without trailing slash /
  2. 63 |
  3. A file extension can be added to force a specific format. For instance /profile/card can be 64 | requested as /profile/card.ttl to request a Turtle file, or /profile/card.json to 65 | request a JSON-LD file 66 |
  4. 67 |
  5. This URL also accepts POST requests
  6. 68 |
  7. This URL only accepts POST requests
  8. 69 |
70 |
71 | HTML; 72 | 73 | return $this->createTemplateResponse($body); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Service/RouterService.php: -------------------------------------------------------------------------------- 1 | container = $container; 29 | $this->router = $router; 30 | } 31 | 32 | final public function populate() : Router 33 | { 34 | $container = $this->container; 35 | $router = $this->router; 36 | 37 | /*/ Default output is HTML, routes should return a Response object /*/ 38 | $strategy = new ApplicationStrategy(); 39 | $strategy->setContainer($container); 40 | $router->setStrategy($strategy); 41 | 42 | /*/ Redirect all HTTP requests to HTTPS, unless we are behind a proxy /*/ 43 | if ( ! getenv('PROXY_MODE')) { 44 | $router->map('GET', '/{page:(?:.|/)*}', HttpToHttpsController::class)->setScheme('http'); 45 | } 46 | 47 | /*/ Map routes and groups /*/ 48 | $router->map('GET', '/', HelloWorldController::class); 49 | $router->group('/profile', $this->createProfileGroup()); 50 | 51 | return $router; 52 | } 53 | 54 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 55 | 56 | private function createProfileGroup() : Callable 57 | { 58 | return static function (RouteGroup $group) { 59 | $group->map('GET', '/', AddSlashToPathController::class); 60 | $group->map('GET', '', ProfileController::class); 61 | $group->map('GET', '/card', CardController::class); 62 | $group->map('GET', '/card{extension}', CardController::class); 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ExceptionResponse.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 25 | } 26 | 27 | final public function createResponse() : HtmlResponse 28 | { 29 | $exception = $this->exception; 30 | 31 | if ($exception instanceof HttpException) { 32 | $response = $this->respondToHttpException($exception); 33 | } else { 34 | $response = $this->responseToException($exception); 35 | } 36 | 37 | return $response; 38 | } 39 | 40 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 41 | 42 | private function isDevelop() : bool 43 | { 44 | static $isDevelop; 45 | 46 | if ($isDevelop === null) { 47 | $isDevelop = getenv('ENVIRONMENT') === 'development'; 48 | } 49 | 50 | return $isDevelop; 51 | } 52 | 53 | private function responseToException(Exception $exception) : HtmlResponse 54 | { 55 | $html = "

Oh-no! The developers messed up!

{$exception->getMessage()}

"; 56 | 57 | if ($this->isDevelop()) { 58 | $html .= 59 | "

{$exception->getFile()}:{$exception->getLine()}

" . 60 | "
{$exception->getTraceAsString()}
"; 61 | } 62 | 63 | return new HtmlResponse($html, 500, []); 64 | } 65 | 66 | private function respondToHttpException(HttpException $exception) : HtmlResponse 67 | { 68 | $status = $exception->getStatusCode(); 69 | 70 | $message = self::MESSAGE_GENERIC_ERROR; 71 | 72 | if ($exception instanceof NotFoundException) { 73 | $message = self::MESSAGE_NO_SUCH_PAGE; 74 | } 75 | 76 | $html = vsprintf('

%s

%s (%s)

', [ 77 | $message, 78 | $exception->getMessage(), 79 | $status, 80 | ]); 81 | 82 | if ($this->isDevelop()) { 83 | $html .= "
{$exception->getTraceAsString()}
"; 84 | } 85 | 86 | return new HtmlResponse($html, $status, $exception->getHeaders()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Controller/AuthorizeController.php: -------------------------------------------------------------------------------- 1 | baseUrl . "/login/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']); 15 | 16 | return $this->getResponse() 17 | ->withHeader("Location", $loginUrl) 18 | ->withStatus(302, "Approval required") 19 | ; 20 | } 21 | 22 | $queryParams = $request->getQueryParams(); 23 | 24 | $parser = new \Lcobucci\JWT\Parser(); 25 | 26 | try { 27 | $token = $parser->parse($request->getQueryParams()['request']); 28 | $_SESSION["nonce"] = $token->getClaim('nonce'); 29 | } catch(\Exception $e) { 30 | $_SESSION["nonce"] = $request->getQueryParams()['nonce']; 31 | } 32 | 33 | /*/ Prepare GET parameters for OAUTH server request /*/ 34 | $getVars = $queryParams; 35 | 36 | $getVars['response_type'] = $this->getResponseType($queryParams); 37 | $getVars['scope'] = "openid" ; 38 | 39 | if (!isset($getVars['grant_type'])) { 40 | $getVars['grant_type'] = 'implicit'; 41 | } 42 | 43 | if (!isset($getVars['redirect_uri'])) { 44 | try { 45 | $getVars['redirect_uri'] = $token->getClaim("redirect_uri"); 46 | } catch(\Exception $e) { 47 | return $this->getResponse() 48 | ->withStatus(400, "Bad request, missing redirect uri") 49 | ; 50 | } 51 | } 52 | 53 | if (! isset($queryParams['client_id'])) { 54 | return $this->getResponse() 55 | ->withStatus(400, "Bad request, missing client_id") 56 | ; 57 | } 58 | 59 | $clientId = $getVars['client_id']; 60 | $approval = $this->checkApproval($clientId); 61 | if (!$approval) { 62 | // FIXME: Generate a proper url for this; 63 | $approvalUrl = $this->baseUrl . "/sharing/$clientId/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']); 64 | 65 | return $this->getResponse() 66 | ->withHeader("Location", $approvalUrl) 67 | ->withStatus(302, "Approval required") 68 | ; 69 | } 70 | 71 | // replace the request getVars with the morphed version 72 | $request = $request->withQueryParams($getVars); 73 | 74 | $user = new \Pdsinterop\Solid\Auth\Entity\User(); 75 | $user->setIdentifier($this->getProfilePage()); 76 | 77 | $response = new \Laminas\Diactoros\Response(); 78 | $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); 79 | 80 | $response = $server->respondToAuthorizationRequest($request, $user, $approval); 81 | 82 | return $this->tokenGenerator->addIdTokenToResponse($response, 83 | $clientId, 84 | $this->getProfilePage(), 85 | $_SESSION['nonce'], 86 | $this->config->getPrivateKey() 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /run-solid-test-suite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function setup { 5 | # Run the Solid test-suite 6 | docker network create testnet 7 | 8 | # Build and start Nextcloud server with code from current repo contents: 9 | docker build -t standalone-solid-server . 10 | 11 | docker build -t cookie https://github.com/pdsinterop/test-suites.git#main:servers/php-solid-server/cookie 12 | docker build -t pubsub-server https://github.com/pdsinterop/php-solid-pubsub-server.git#main 13 | 14 | docker pull solidtestsuite/webid-provider-tests:v2.0.3 15 | docker tag solidtestsuite/webid-provider-tests:v2.0.3 webid-provider-tests 16 | docker pull solidtestsuite/solid-crud-tests:v6.0.0 17 | docker tag solidtestsuite/solid-crud-tests:v6.0.0 solid-crud-tests 18 | docker pull solidtestsuite/web-access-control-tests:v7.0.0 19 | docker tag solidtestsuite/web-access-control-tests:v7.0.0 web-access-control-tests 20 | } 21 | 22 | function runPss { 23 | docker run -d --name server --network=testnet --env-file ./env-vars-for-test-image.list standalone-solid-server 24 | docker run -d --name thirdparty --network=testnet --env-file ./env-vars-for-third-party.list standalone-solid-server 25 | 26 | docker run -d --name pubsub --network=testnet pubsub-server 27 | 28 | until docker run --rm --network=testnet webid-provider-tests curl -kI https://server 2> /dev/null > /dev/null 29 | do 30 | echo Waiting for server to start, this can take up to a minute ... 31 | docker ps -a 32 | docker logs server 33 | sleep 1 34 | done 35 | docker ps -a 36 | docker logs server 37 | echo Confirmed that https://server is started now 38 | 39 | echo Getting cookie for Alice... 40 | export COOKIE="`docker run --rm --cap-add=SYS_ADMIN --network=testnet -e SERVER_TYPE=php-solid-server --env-file ./env-vars-for-test-image.list cookie`" 41 | if [[ $COOKIE == PHPSESSID* ]] 42 | then 43 | echo Successfully obtained cookie for Alice: $COOKIE 44 | else 45 | echo Error obtaining cookie for Alice, stopping. 46 | exit 1 47 | fi 48 | 49 | until docker run --rm --network=testnet webid-provider-tests curl -kI https://thirdparty 2> /dev/null > /dev/null 50 | do 51 | echo Waiting for thirdparty to start, this can take up to a minute ... 52 | docker ps -a 53 | docker logs thirdparty 54 | sleep 1 55 | done 56 | docker ps -a 57 | docker logs thirdparty 58 | echo Confirmed that https://thirdparty is started now 59 | 60 | echo Getting cookie for Bob... 61 | export COOKIE_BOB="`docker run --rm --cap-add=SYS_ADMIN --network=testnet -e SERVER_TYPE=php-solid-server --env-file ./env-vars-for-third-party.list cookie`" 62 | if [[ $COOKIE_BOB == PHPSESSID* ]] 63 | then 64 | echo Successfully obtained cookie for Bob: $COOKIE_BOB 65 | else 66 | echo Error obtaining cookie for Bob, stopping. 67 | exit 1 68 | fi 69 | } 70 | 71 | function runTests { 72 | echo "Running webid-provider tests with cookie $COOKIE" 73 | docker run --rm --network=testnet --env COOKIE="$COOKIE" --env-file ./env-vars-for-test-image.list webid-provider-tests 74 | docker run --rm --network=testnet --env COOKIE="$COOKIE" --env-file ./env-vars-for-test-image.list solid-crud-tests 75 | docker run --rm --network=testnet --env COOKIE="$COOKIE" --env COOKIE_ALICE="$COOKIE" --env COOKIE_BOB="$COOKIE_BOB" --env-file ./env-vars-for-test-image.list web-access-control-tests 76 | } 77 | 78 | function teardown { 79 | docker stop `docker ps --filter network=testnet -q` 80 | docker rm `docker ps --filter network=testnet -qa` 81 | docker network remove testnet 82 | } 83 | 84 | teardown || true 85 | setup 86 | runPss 87 | runTests 88 | # teardown 89 | -------------------------------------------------------------------------------- /tests/fixtures/foaf.rdf: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Peachey 20 | Ben 21 | 22 | 23 | 24 | Alice 25 | 9e9c84204ba63aa49a664273c5563c0cd78cc9ea 26 | 27 | 28 | 29 | 30 | 31 | Bob 32 | 1a9daad476f0158b81bc66b7b27b438b4b4c19c0 33 | 34 | 35 | 36 | 012e360c88b2bf940e6a52de3e5bbf59ccbdada6 37 | Ben Peachey 38 | Potherca 39 | 40 | 41 | Mr. 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Bobby 61 | Bob 62 | Bob Bobby 63 | Bobbers 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/Controller/ResourceController.php: -------------------------------------------------------------------------------- 1 | server = $server; 29 | $this->DPop = new DPop(); 30 | $this->WAC = new WAC($server->getFilesystem()); 31 | 32 | // Make sure the root folder has an acl file, as is required by the spec; 33 | // Generate a default file granting the owner full access if there is nothing there. 34 | if (!$server->getFilesystem()->has("/storage/.acl")) { 35 | $defaultAcl = $this->generateDefaultAcl(); 36 | $server->getFilesystem()->write("/storage/.acl", $defaultAcl); 37 | } 38 | } 39 | 40 | final public function __invoke(Request $request, array $args) : Response 41 | { 42 | try { 43 | $webId = $this->DPop->getWebId($request); 44 | } catch(\Exception $e) { 45 | return $this->server->getResponse()->withStatus(409, 'Invalid token'); 46 | } 47 | 48 | $allowedOrigins = $this->config->getAllowedOrigins(); 49 | $origins = $request->getHeader('Origin'); 50 | 51 | if ($origins !== []) { 52 | foreach ($origins as $origin) { 53 | if ($this->WAC->isAllowed($request, $webId, $origin, $allowedOrigins)) { 54 | $response = $this->server->respondToRequest($request); 55 | 56 | return $this->WAC->addWACHeaders($request, $response, $webId); 57 | } 58 | } 59 | return $this->server->getResponse()->withStatus(403, 'Access denied'); 60 | } else { 61 | $response = $this->server->respondToRequest($request); 62 | 63 | return $this->WAC->addWACHeaders($request, $response, $webId); 64 | } 65 | } 66 | 67 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 68 | 69 | private function generateDefaultAcl() 70 | { 71 | $defaultProfile = <<< EOF 72 | # Root ACL resource for the user account 73 | @prefix acl: . 74 | @prefix foaf: . 75 | 76 | <#public> 77 | a acl:Authorization; 78 | acl:agentClass foaf:Agent; 79 | acl:accessTo ; 80 | acl:default ; 81 | acl:mode 82 | acl:Read. 83 | 84 | # The owner has full access to every resource in their pod. 85 | # Other agents have no access rights, 86 | # unless specifically authorized in other .acl resources. 87 | <#owner> 88 | a acl:Authorization; 89 | acl:agent <{user-profile-uri}>; 90 | # Set the access to the root storage folder itself 91 | acl:accessTo ; 92 | # All resources will inherit this authorization, by default 93 | acl:default ; 94 | # The owner has all of the access modes allowed 95 | acl:mode 96 | acl:Read, acl:Write, acl:Control. 97 | EOF; 98 | 99 | $profileUri = $this->getUserProfile(); 100 | 101 | return str_replace("{user-profile-uri}", $profileUri, $defaultProfile); 102 | } 103 | 104 | private function getUserProfile() { 105 | return $this->baseUrl . "/profile/card#me"; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Service/ContainerService.php: -------------------------------------------------------------------------------- 1 | container = $container; 35 | } 36 | 37 | final public function populate() : Container 38 | { 39 | $container = $this->container; 40 | 41 | /*/ Wire objects together /*/ 42 | $container->delegate(new ReflectionContainer()); 43 | 44 | $container->add(ServerRequestInterface::class, Request::class); 45 | $container->add(ResponseInterface::class, Response::class); 46 | 47 | $container->share(FilesystemInterface::class, $this->createFilesystem()); 48 | 49 | $container->share(PHPTAL::class, $this->createTemplate()); 50 | 51 | $this->addControllers($container); 52 | 53 | return $container; 54 | } 55 | 56 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 57 | 58 | private function addControllers(Container $container) : void 59 | { 60 | $controllers = [ 61 | AddSlashToPathController::class, 62 | CardController::class, 63 | HelloWorldController::class, 64 | HttpToHttpsController::class, 65 | ProfileController::class, 66 | ]; 67 | 68 | $traits = [ 69 | 'setFilesystem' => [FilesystemInterface::class], 70 | 'setResponse' => [ResponseInterface::class], 71 | 'setTemplate' => [PHPTAL::class], 72 | ]; 73 | 74 | $traitMethods = array_keys($traits); 75 | 76 | array_walk($controllers, static function ($controller) use ($container, $traits, $traitMethods) { 77 | $definition = $container->add($controller); 78 | 79 | $methods = get_class_methods($controller); 80 | 81 | array_walk($methods, static function ($method) use ($definition, $traitMethods, $traits) { 82 | if (in_array($method, $traitMethods, true)) { 83 | $definition->addMethodCall($method, $traits[$method]); 84 | } 85 | }); 86 | }); 87 | } 88 | 89 | private function createFilesystem() : Callable 90 | { 91 | return static function () { 92 | // @FIXME: Filesystem root and the $adapter should be configurable. 93 | // Implement this with `$filesystem = \MJRider\FlysystemFactory\create(getenv('STORAGE_ENDPOINT'));` 94 | $filesystemRoot = __DIR__ . '/../../tests/fixtures/'; 95 | 96 | $adapter = new Local($filesystemRoot); 97 | 98 | $filesystem = new Filesystem($adapter); 99 | $graph = new EasyRdf_Graph(); 100 | $plugin = new ReadRdf($graph); 101 | $filesystem->addPlugin($plugin); 102 | 103 | return $filesystem; 104 | }; 105 | } 106 | 107 | private function createTemplate() : Callable 108 | { 109 | return static function () { 110 | $template = new PHPTAL(); 111 | $template->setTemplateRepository(__DIR__ . '/../Template'); 112 | 113 | return $template; 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Controller/Profile/CardController.php: -------------------------------------------------------------------------------- 1 | getRequestTarget() 31 | $filePath = '/foaf.rdf'; 32 | $filesystem = $this->getFilesystem(); 33 | $extension = '.ttl'; 34 | 35 | // @TODO: Content negotiation from Accept headers 36 | //$format = $request->getHeader('Accept'); 37 | 38 | if (array_key_exists('extension', $args)) { 39 | $extension = $args['extension']; 40 | } 41 | 42 | $format = $this->getFormatForExtension($extension); 43 | 44 | if (in_array($format, Format::keys()) === false) { 45 | throw new NotFoundException($request->getRequestTarget()); 46 | } 47 | 48 | $contentType = $this->getContentTypeForFormat($format); 49 | 50 | $url = (string) $request->getUri(); 51 | 52 | if (substr($url, -strlen($extension)) === $extension) { 53 | $url = substr($url, 0, -strlen($extension)); 54 | } 55 | 56 | try { 57 | $content = $filesystem->readRdf($filePath, $format, $url); 58 | } catch (\Pdsinterop\Rdf\Flysystem\Exception $exception) { 59 | $content = $exception->getMessage(); 60 | 61 | return $this->createTextResponse($content)->withStatus(500); 62 | } 63 | 64 | return $this->createTextResponse($content)->withHeader('Content-Type', $contentType); 65 | } 66 | 67 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 68 | 69 | /** 70 | * @param string $format 71 | * 72 | * @return string 73 | */ 74 | private function getContentTypeForFormat(string $format) : string 75 | { 76 | $contentType = ''; 77 | 78 | switch ($format) { 79 | case Format::JSON_LD: 80 | $contentType = 'application/ld+json'; 81 | break; 82 | 83 | case Format::N_TRIPLES: 84 | $contentType = 'application/n-triples'; 85 | break; 86 | 87 | case Format::NOTATION_3: 88 | $contentType = 'text/n3;charset=utf-8'; 89 | break; 90 | 91 | case Format::RDF_XML: 92 | $contentType = 'application/rdf+xml'; 93 | break; 94 | 95 | case Format::TURTLE: 96 | $contentType = 'text/turtle'; 97 | break; 98 | 99 | default: 100 | break; 101 | } 102 | 103 | return $contentType; 104 | } 105 | 106 | /** 107 | * @param string $extension 108 | * 109 | * @return string 110 | */ 111 | private function getFormatForExtension(string $extension) : string 112 | { 113 | $format = ''; 114 | 115 | switch ($extension) { 116 | case '.json': 117 | case '.jsonld': 118 | $format = Format::JSON_LD; 119 | break; 120 | 121 | case '.nt': 122 | $format = Format::N_TRIPLES; 123 | break; 124 | 125 | case '.n3': 126 | $format = Format::NOTATION_3; 127 | break; 128 | 129 | case '.xml': 130 | case '.rdf': 131 | $format = Format::RDF_XML; 132 | break; 133 | 134 | case '.ttl': 135 | $format = Format::TURTLE; 136 | break; 137 | 138 | default: 139 | break; 140 | } 141 | 142 | return $format; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Controller/ServerController.php: -------------------------------------------------------------------------------- 1 | config = new \Pdsinterop\Solid\ServerConfig(__DIR__.'/../../config/'); 24 | 25 | $this->authServerConfig = $this->createAuthServerConfig(); 26 | $this->authServerFactory = (new \Pdsinterop\Solid\Auth\Factory\AuthorizationServerFactory($this->authServerConfig))->create(); 27 | $this->tokenGenerator = (new \Pdsinterop\Solid\Auth\TokenGenerator($this->authServerConfig)); 28 | $this->baseUrl = isset($_ENV['SERVER_ROOT']) ? $_ENV['SERVER_ROOT'] : "https://localhost"; 29 | } 30 | 31 | public function getOpenIdEndpoints() { 32 | // FIXME: would be better to base this on the available routes if possible. 33 | $this->baseUrl = isset($_ENV['SERVER_ROOT']) ? $_ENV['SERVER_ROOT'] : "https://localhost"; 34 | return [ 35 | 'issuer' => $this->baseUrl, 36 | 'authorization_endpoint' => $this->baseUrl . "/authorize", 37 | 'jwks_uri' => $this->baseUrl . "/jwks", 38 | "check_session_iframe" => $this->baseUrl . "/session", 39 | "end_session_endpoint" => $this->baseUrl . "/logout", 40 | "token_endpoint" => $this->baseUrl . "/token", 41 | "userinfo_endpoint" => $this->baseUrl . "/userinfo", 42 | "registration_endpoint" => $this->baseUrl . "/register", 43 | ]; 44 | } 45 | 46 | public function getKeys() 47 | { 48 | $encryptionKey = $this->config->getEncryptionKey(); 49 | $privateKey = $this->config->getPrivateKey(); 50 | $key = openssl_pkey_get_private($privateKey); 51 | $publicKey = openssl_pkey_get_details($key)['key']; 52 | 53 | return [ 54 | "encryptionKey" => $encryptionKey, 55 | "privateKey" => $privateKey, 56 | "publicKey" => $publicKey, 57 | ]; 58 | } 59 | 60 | public function createAuthServerConfig() 61 | { 62 | $clientId = $_GET['client_id']; // FIXME: No request object here to get the client Id from. 63 | $client = $this->getClient($clientId); 64 | $keys = $this->getKeys(); 65 | 66 | return (new ConfigFactory( 67 | $client, 68 | $keys['encryptionKey'], 69 | $keys['privateKey'], 70 | $keys['publicKey'], 71 | $this->getOpenIdEndpoints() 72 | ))->create(); 73 | } 74 | 75 | public function getClient($clientId) 76 | { 77 | $clientRegistration = $this->config->getClientRegistration($clientId); 78 | 79 | if ($clientId && count($clientRegistration)) { 80 | $client = new Client( 81 | $clientId, 82 | $clientRegistration['client_secret'], 83 | $clientRegistration['redirect_uris'], 84 | $clientRegistration['client_name'] 85 | ); 86 | } else { 87 | $client = new Client('', '', [], ''); 88 | } 89 | 90 | return $client; 91 | } 92 | 93 | public function createConfig() 94 | { 95 | $clientId = $_GET['client_id']; 96 | $client = $this->getClient($clientId); 97 | 98 | return (new ConfigFactory( 99 | $client, 100 | $this->keys['encryptionKey'], 101 | $this->keys['privateKey'], 102 | $this->keys['publicKey'], 103 | $this->openIdConfiguration 104 | ))->create(); 105 | } 106 | 107 | public function checkApproval($clientId) 108 | { 109 | $approval = Authorization::DENIED; 110 | 111 | $allowedClients = $this->config->getAllowedClients($this->userId); 112 | 113 | if ( 114 | $clientId === md5("tester") // FIXME: Double check that this is not a security issue; It is only here to help the test suite; 115 | || in_array($clientId, $allowedClients, true) 116 | ) { 117 | $approval = Authorization::APPROVED; 118 | } 119 | 120 | return $approval; 121 | } 122 | 123 | public function getProfilePage() : string 124 | { 125 | return $this->baseUrl . "/profile/card#me"; // FIXME: would be better to base this on the available routes if possible. 126 | } 127 | 128 | public function getResponseType($params) : string 129 | { 130 | $responseTypes = explode(" ", $params['response_type'] ?? ''); 131 | 132 | foreach ($responseTypes as $responseType) { 133 | switch ($responseType) { 134 | case "token": 135 | return "token"; 136 | break; 137 | case "code": 138 | return "code"; 139 | break; 140 | } 141 | } 142 | 143 | return "token"; // default to token response type; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/ServerConfig.php: -------------------------------------------------------------------------------- 1 | path = $path; 17 | $this->serverConfigFile = $this->path . "serverConfig.json"; 18 | $this->userConfigFile = $this->path . "user.json"; 19 | $this->serverConfig = $this->loadConfig(); 20 | $this->userConfig = $this->loadUserConfig(); 21 | 22 | } 23 | 24 | public function getAllowedOrigins() 25 | { 26 | $allowedOrigins = []; 27 | 28 | $serverConfig = $this->serverConfig; 29 | foreach ($serverConfig as $value) { 30 | if (isset($value['redirect_uris'])) { 31 | foreach ($value['redirect_uris'] as $url) { 32 | $allowedOrigins[] = parse_url($url)['host']; 33 | } 34 | } 35 | } 36 | 37 | return array_unique($allowedOrigins); 38 | } 39 | 40 | private function loadConfig() 41 | { 42 | if ( ! file_exists($this->serverConfigFile)) { 43 | $keySet = $this->generateKeySet(); 44 | $this->serverConfig = [ 45 | "encryptionKey" => $keySet['encryptionKey'], 46 | "privateKey" => $keySet['privateKey'], 47 | ]; 48 | $this->saveConfig(); 49 | } 50 | 51 | return json_decode(file_get_contents($this->serverConfigFile), true); 52 | } 53 | 54 | private function saveConfig() 55 | { 56 | file_put_contents($this->serverConfigFile, json_encode($this->serverConfig, JSON_PRETTY_PRINT)); 57 | } 58 | 59 | private function loadUserConfig() 60 | { 61 | if ( ! file_exists($this->userConfigFile)) { 62 | $this->userConfig = [ 63 | "allowedClients" => [], 64 | ]; 65 | $this->saveUserConfig(); 66 | } 67 | 68 | return json_decode(file_get_contents($this->userConfigFile), true); 69 | } 70 | 71 | private function saveUserConfig() 72 | { 73 | file_put_contents($this->userConfigFile, json_encode($this->userConfig, JSON_PRETTY_PRINT)); 74 | } 75 | 76 | /* Server data */ 77 | public function getPrivateKey() 78 | { 79 | return $this->serverConfig['privateKey']; 80 | } 81 | 82 | public function getEncryptionKey() 83 | { 84 | return $this->serverConfig['encryptionKey']; 85 | } 86 | 87 | public function getClientConfigById($clientId) 88 | { 89 | $clients = (array) $this->serverConfig['clients']; 90 | 91 | if (array_key_exists($clientId, $clients)) { 92 | return $clients[$clientId]; 93 | } 94 | 95 | return null; 96 | } 97 | 98 | public function saveClientConfig($clientConfig) 99 | { 100 | $clientId = uuidv4(); 101 | $this->serverConfig['clients'][$clientId] = $clientConfig; 102 | $this->saveConfig(); 103 | 104 | return $clientId; 105 | } 106 | 107 | public function saveClientRegistration($origin, $clientData) 108 | { 109 | $originHash = md5($origin); 110 | $existingRegistration = $this->getClientRegistration($originHash); 111 | if ($existingRegistration && isset($existingRegistration['client_name'])) { 112 | return $originHash; 113 | } 114 | 115 | $clientData['client_name'] = $origin; 116 | $clientData['client_secret'] = md5(random_bytes(32)); 117 | $this->serverConfig['client-' . $originHash] = $clientData; 118 | $this->saveConfig(); 119 | 120 | return $originHash; 121 | } 122 | 123 | public function getClientRegistration($clientId) 124 | { 125 | if (isset($this->serverConfig['client-' . $clientId])) { 126 | return $this->serverConfig['client-' . $clientId]; 127 | } else { 128 | return []; 129 | } 130 | } 131 | 132 | /* User specific data */ 133 | public function getAllowedClients() 134 | { 135 | return $this->userConfig['allowedClients']; 136 | } 137 | 138 | public function addAllowedClient($userId, $clientId) 139 | { 140 | $this->userConfig['allowedClients'][] = $clientId; 141 | $this->userConfig['allowedClients'] = array_unique($this->userConfig['allowedClients']); 142 | $this->saveUserConfig(); 143 | } 144 | 145 | public function removeAllowedClient($userId, $clientId) 146 | { 147 | $this->userConfig['allowedClients'] = array_diff($this->userConfig['allowedClients'], [$clientId]); 148 | $this->saveUserConfig(); 149 | } 150 | 151 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 152 | 153 | private function generateKeySet() 154 | { 155 | $config = [ 156 | "digest_alg" => "sha256", 157 | "private_key_bits" => 2048, 158 | "private_key_type" => OPENSSL_KEYTYPE_RSA, 159 | ]; 160 | // Create the private and public key 161 | $key = openssl_pkey_new($config); 162 | 163 | // Extract the private key from $key to $privateKey 164 | openssl_pkey_export($key, $privateKey); 165 | $encryptionKey = base64_encode(random_bytes(32)); 166 | $result = [ 167 | "privateKey" => $privateKey, 168 | "encryptionKey" => $encryptionKey, 169 | ]; 170 | 171 | return $result; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.6.2](https://github.com/pdsinterop/php-solid-server/tree/v0.6.2) (2022-01-13) 4 | 5 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.6.1...v0.6.2) 6 | 7 | ## [v0.6.1](https://github.com/pdsinterop/php-solid-server/tree/v0.6.1) (2022-01-12) 8 | 9 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.6.0...v0.6.1) 10 | 11 | ### Pull request(s) without label 12 | 13 | - Code cleanup [\#59](https://github.com/pdsinterop/php-solid-server/pull/59) (@Potherca) 14 | 15 | ## [v0.6.0](https://github.com/pdsinterop/php-solid-server/tree/v0.6.0) (2022-01-11) 16 | 17 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.5.2...v0.6.0) 18 | 19 | ### Closes 20 | 21 | - Cannot assign requested address [\#26](https://github.com/pdsinterop/php-solid-server/issues/26) 22 | - New release \(v0.6.0\) [\#51](https://github.com/pdsinterop/php-solid-server/issues/51) 23 | - Which features are a available and which are not? [\#46](https://github.com/pdsinterop/php-solid-server/issues/46) 24 | - when ENVIRONMENT=development is set, the server does not work on https [\#16](https://github.com/pdsinterop/php-solid-server/issues/16) 25 | 26 | ### Pull request(s) without label 27 | 28 | - Cleanup for v0.6 release. [\#58](https://github.com/pdsinterop/php-solid-server/pull/58) (@Potherca) 29 | - Run latest versions of test suites [\#53](https://github.com/pdsinterop/php-solid-server/pull/53) (@michielbdejong) 30 | 31 | ## [v0.5.2](https://github.com/pdsinterop/php-solid-server/tree/v0.5.2) (2021-11-26) 32 | 33 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.5.1...v0.5.2) 34 | 35 | ### Closes 36 | 37 | - Instructions for running with docker-compose [\#40](https://github.com/pdsinterop/php-solid-server/issues/40) 38 | - Tests failing in master [\#34](https://github.com/pdsinterop/php-solid-server/issues/34) 39 | - New webid-provider test failing in master [\#31](https://github.com/pdsinterop/php-solid-server/issues/31) 40 | 41 | ### Pull request(s) without label 42 | 43 | - Latest test suites [\#50](https://github.com/pdsinterop/php-solid-server/pull/50) (@michielbdejong) 44 | - Pin test suite versions [\#49](https://github.com/pdsinterop/php-solid-server/pull/49) (@michielbdejong) 45 | - Switch from Travis to GitHub Actions [\#47](https://github.com/pdsinterop/php-solid-server/pull/47) (@michielbdejong) 46 | - adding origin check with WAC [\#44](https://github.com/pdsinterop/php-solid-server/pull/44) (@ylebre) 47 | - Fix some outdated documentation and scripts [\#41](https://github.com/pdsinterop/php-solid-server/pull/41) (@NoelDeMartin) 48 | - Merge pull request \#33 from pdsinterop/add-wac-tests [\#39](https://github.com/pdsinterop/php-solid-server/pull/39) (@ylebre) 49 | - Add wac tests [\#38](https://github.com/pdsinterop/php-solid-server/pull/38) (@poef) 50 | - WIP: dpop/webid [\#37](https://github.com/pdsinterop/php-solid-server/pull/37) (@ylebre) 51 | - Updating branch before starting [\#36](https://github.com/pdsinterop/php-solid-server/pull/36) (@ylebre) 52 | - Fix solid tests [\#35](https://github.com/pdsinterop/php-solid-server/pull/35) (@michielbdejong) 53 | - Add WAC tests [\#33](https://github.com/pdsinterop/php-solid-server/pull/33) (@michielbdejong) 54 | - Use the latest version of the webid-provider-tests [\#32](https://github.com/pdsinterop/php-solid-server/pull/32) (@michielbdejong) 55 | - Pull from Docker hub [\#30](https://github.com/pdsinterop/php-solid-server/pull/30) (@michielbdejong) 56 | - Add solid-crud tests [\#20](https://github.com/pdsinterop/php-solid-server/pull/20) (@michielbdejong) 57 | 58 | ## [v0.5.1](https://github.com/pdsinterop/php-solid-server/tree/v0.5.1) (2020-11-12) 59 | 60 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.5.0...v0.5.1) 61 | 62 | ### Pull request(s) without label 63 | 64 | - Fix contributing link in README [\#29](https://github.com/pdsinterop/php-solid-server/pull/29) (@NoelDeMartin) 65 | 66 | ## [v0.5.0](https://github.com/pdsinterop/php-solid-server/tree/v0.5.0) (2020-11-09) 67 | 68 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.4.0...v0.5.0) 69 | 70 | ### Closes 71 | 72 | - openid-configuration announces "localhost" instead of "server" [\#25](https://github.com/pdsinterop/php-solid-server/issues/25) 73 | - /.well-known/openid-configuration broken in master? [\#24](https://github.com/pdsinterop/php-solid-server/issues/24) 74 | 75 | ### Pull request(s) without label 76 | 77 | - Update test runner [\#27](https://github.com/pdsinterop/php-solid-server/pull/27) (@michielbdejong) 78 | 79 | ## [v0.4.0](https://github.com/pdsinterop/php-solid-server/tree/v0.4.0) (2020-10-23) 80 | 81 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.3.0...v0.4.0) 82 | 83 | ### Closes 84 | 85 | - config file not found? [\#19](https://github.com/pdsinterop/php-solid-server/issues/19) 86 | - Trailing slash in issuer [\#22](https://github.com/pdsinterop/php-solid-server/issues/22) 87 | 88 | ### Pull request(s) without label 89 | 90 | - Improve tests [\#21](https://github.com/pdsinterop/php-solid-server/pull/21) (@michielbdejong) 91 | - added token response with id token [\#17](https://github.com/pdsinterop/php-solid-server/pull/17) (@ylebre) 92 | - Run test suite on Travis CI [\#15](https://github.com/pdsinterop/php-solid-server/pull/15) (@michielbdejong) 93 | - Change flysystem-rdf dependency to dev-master as dev-dev does not exist. [\#23](https://github.com/pdsinterop/php-solid-server/pull/23) (@Potherca) 94 | - Add resources api calls [\#18](https://github.com/pdsinterop/php-solid-server/pull/18) (@Potherca) 95 | 96 | ## [v0.3.0](https://github.com/pdsinterop/php-solid-server/tree/v0.3.0) (2020-09-28) 97 | 98 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.2.0...v0.3.0) 99 | 100 | ### Closes 101 | 102 | - `HOST=127.0.0.1` vs `HOST=localhost` [\#8](https://github.com/pdsinterop/php-solid-server/issues/8) 103 | - possible helpful codebase [\#3](https://github.com/pdsinterop/php-solid-server/issues/3) 104 | - Add /.well-known/openid-configuration endpoint [\#6](https://github.com/pdsinterop/php-solid-server/issues/6) 105 | 106 | ### Pull request(s) without label 107 | 108 | - WIP: achieve login with solid.community [\#14](https://github.com/pdsinterop/php-solid-server/pull/14) (@ylebre) 109 | - merge dev branch [\#13](https://github.com/pdsinterop/php-solid-server/pull/13) (@michielbdejong) 110 | - Authorize end point [\#11](https://github.com/pdsinterop/php-solid-server/pull/11) (@michielbdejong) 111 | 112 | ## [v0.2.0](https://github.com/pdsinterop/php-solid-server/tree/v0.2.0) (2020-09-12) 113 | 114 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.1.1...v0.2.0) 115 | 116 | ### Pull request(s) without label 117 | 118 | - Add templating [\#5](https://github.com/pdsinterop/php-solid-server/pull/5) (@Potherca) 119 | - Second draft: Hard-coded profile card [\#2](https://github.com/pdsinterop/php-solid-server/pull/2) (@Potherca) 120 | 121 | ## [v0.1.1](https://github.com/pdsinterop/php-solid-server/tree/v0.1.1) (2020-07-01) 122 | 123 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.1.0...v0.1.1) 124 | 125 | ## [v0.1.0](https://github.com/pdsinterop/php-solid-server/tree/v0.1.0) (2020-06-30) 126 | 127 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.0.0...v0.1.0) 128 | 129 | ### Pull request(s) without label 130 | 131 | - First draft [\#1](https://github.com/pdsinterop/php-solid-server/pull/1) (@Potherca) 132 | 133 | ## [v0.0.0](https://github.com/pdsinterop/php-solid-server/tree/v0.0.0) (2020-06-12) 134 | 135 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/8e7c63f389ea45da60141cd1a4f59ff467046268...v0.0.0) 136 | 137 | 138 | 139 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 140 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Project Specific 3 | # ------------------------------------------------------------------------------ 4 | user=pdsinterop 5 | project=php-solid-server 6 | # ------------------------------------------------------------------------------ 7 | breaking-labels=backwards-incompatible, Backwards incompatible, breaking 8 | bug-labels=bug 9 | deprecated-labels=deprecated, Deprecated, Type: Deprecated 10 | enhancement-labels=improvement, documentation, enhancement 11 | exclude-labels=question, duplicate,invalid 12 | removed-labels=removed, Removed, Type: Removed 13 | security-labels=security, Security, Type: Security 14 | summary-labels=Release summary, release-summary, Summary, summary 15 | unreleased-label=Unreleased 16 | # ============================================================================== 17 | 18 | # ============================================================================== 19 | # Organisation-wide 20 | # ------------------------------------------------------------------------------ 21 | # Output and content related 22 | # ------------------------------------------------------------------------------ 23 | date-format=%Y-%m-%d 24 | output=CHANGELOG.md 25 | header=# Changelog 26 | breaking-prefix=### Breaking changes 27 | bug-prefix=### Fixes 28 | deprecated-prefix=### Deprecates 29 | enhancement-prefix=### Changes 30 | issue-prefix=### Closes 31 | merge-prefix=### Pull request(s) without label 32 | removed-prefix=### Removes 33 | security-prefix=### Security 34 | # ------------------------------------------------------------------------------ 35 | add-issues-wo-labels=true 36 | add-pr-wo-labels=true 37 | author=true 38 | compare-link=true 39 | filter-issues-by-milestone=true 40 | # http-cache=true 41 | issues=true 42 | pulls=true 43 | # unreleased-only=true 44 | unreleased=true 45 | usernames-as-github-logins=true 46 | verbose=false 47 | # ============================================================================== 48 | 49 | ;user USER Username of the owner of the target GitHub repo OR the namespace of target Github repo if owned by an organization. 50 | ;project PROJECT Name of project on GitHub. 51 | ;token TOKEN To make more than 50 requests per hour your GitHub token is required. You can generate it at: https://github.com/settings/tokens/new 52 | ;date-format FORMAT Date format. Default is %Y-%m-%d. 53 | ;output NAME Output file. To print to STDOUT instead, use blank as path. Default is CHANGELOG.md 54 | ;base NAME Optional base file to append generated changes to. Default is HISTORY.md 55 | ;summary-label LABEL Set up custom label for the release summary section. Default is "". 56 | ;breaking-label LABEL Set up custom label for the breaking changes section. Default is "**Breaking changes:**". 57 | ;enhancement-label LABEL Set up custom label for enhancements section. Default is "**Implemented enhancements:**". 58 | ;bugs-label LABEL Set up custom label for bug-fixes section. Default is "**Fixed bugs:**". 59 | ;deprecated-label LABEL Set up custom label for the deprecated changes section. Default is "**Deprecated:**". 60 | ;removed-label LABEL Set up custom label for the removed changes section. Default is "**Removed:**". 61 | ;security-label LABEL Set up custom label for the security changes section. Default is "**Security fixes:**". 62 | ;issues-label LABEL Set up custom label for closed-issues section. Default is "**Closed issues:**". 63 | ;header-label LABEL Set up custom header label. Default is "# Changelog". 64 | ;configure-sections STRING Define your own set of sections which overrides all default sections. 65 | ;add-sections HASH, STRING Add new sections but keep the default sections. 66 | ;front-matter JSON Add YAML front matter. Formatted as JSON because it's easier to add on the command line. 67 | ;pr-label LABEL Set up custom label for pull requests section. Default is "**Merged pull requests:**". 68 | ;issues Include closed issues in changelog. Default is true. 69 | ;issues-wo-labels Include closed issues without labels in changelog. Default is true. 70 | ;pr-wo-labels Include pull requests without labels in changelog. Default is true. 71 | ;pull-requests Include pull-requests in changelog. Default is true. 72 | ;filter-by-milestone Use milestone to detect when issue was resolved. Default is true. 73 | ;issues-of-open-milestones Include issues of open milestones. Default is true. 74 | ;author Add author of pull request at the end. Default is true. 75 | ;usernames-as-github-logins Use GitHub tags instead of Markdown links for the author of an issue or pull-request. 76 | ;unreleased-only Generate log from unreleased closed issues only. 77 | ;unreleased Add to log unreleased closed issues. Default is true. 78 | ;unreleased-label LABEL Set up custom label for unreleased closed issues section. Default is "**Unreleased:**". 79 | ;compare-link Include compare link (Full Changelog) between older version and newer version. Default is true. 80 | ;include-labels x,y,z Of the labeled issues, only include the ones with the specified labels. 81 | ;exclude-labels x,y,z Issues with the specified labels will be excluded from changelog. Default is 'duplicate,question,invalid,wontfix'. 82 | ;summary-labels x,y,z Issues with these labels will be added to a new section, called "Release Summary". The section display only body of issues. Default is 'release-summary,summary'. 83 | ;breaking-labels x,y,z Issues with these labels will be added to a new section, called "Breaking changes". Default is 'backwards-incompatible,breaking'. 84 | ;enhancement-labels x,y,z Issues with the specified labels will be added to "Implemented enhancements" section. Default is 'enhancement,Enhancement'. 85 | ;bug-labels x,y,z Issues with the specified labels will be added to "Fixed bugs" section. Default is 'bug,Bug'. 86 | ;deprecated-labels x,y,z Issues with the specified labels will be added to a section called "Deprecated". Default is 'deprecated,Deprecated'. 87 | ;removed-labels x,y,z Issues with the specified labels will be added to a section called "Removed". Default is 'removed,Removed'. 88 | ;security-labels x,y,z Issues with the specified labels will be added to a section called "Security fixes". Default is 'security,Security'. 89 | ;issue-line-labels x,y,z The specified labels will be shown in brackets next to each matching issue. Use "ALL" to show all labels. Default is []. 90 | ;include-tags-regex REGEX Apply a regular expression on tag names so that they can be included, for example: --include-tags-regex ".*+d{1,}". 91 | ;exclude-tags x,y,z Changelog will exclude specified tags 92 | ;exclude-tags-regex REGEX Apply a regular expression on tag names so that they can be excluded, for example: --exclude-tags-regex ".*+d{1,}". 93 | ;since-tag x Changelog will start after specified tag. 94 | ;due-tag x Changelog will end before specified tag. 95 | ;since-commit x Fetch only commits after this time. eg. "2017-01-01 10:00:00" 96 | ;max-issues NUMBER Maximum number of issues to fetch from GitHub. Default is unlimited. 97 | ;release-url URL The URL to point to for release links, in printf format (with the tag as variable). 98 | ;github-site URL The Enterprise GitHub site where your project is hosted. 99 | ;github-api URL The enterprise endpoint to use for your GitHub API. 100 | ;simple-list Create a simple list from issues and pull requests. Default is false. 101 | ;future-release VERSION Put the unreleased changes in the specified release number. 102 | ;release-branch BRANCH Limit pull requests to the release branch, such as master or release. 103 | ;http-cache Use HTTP Cache to cache GitHub API requests (useful for large repos). Default is true. 104 | ;cache-file CACHE-FILE Filename to use for cache. Default is github-changelog-http-cache in a temporary directory. 105 | ;cache-log CACHE-LOG Filename to use for cache log. Default is github-changelog-logger.log in a temporary directory. 106 | ;config-file CONFIG-FILE Path to configuration file. Default is .github_changelog_generator. 107 | ;ssl-ca-file PATH Path to cacert.pem file. Default is a bundled lib/github_changelog_generator/ssl_certs/cacert.pem. Respects SSL_CA_PATH. 108 | ;require x,y,z Path to Ruby file(s) to require before generating changelog. 109 | ;verbose Run verbosely. Default is true. 110 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | delegate(new ReflectionContainer()); 54 | 55 | $container->add(ServerRequestInterface::class, Request::class); 56 | $container->add(ResponseInterface::class, Response::class); 57 | 58 | $container->share(FilesystemInterface::class, function () use ($request) { 59 | // @FIXME: Filesystem $adapter should be configurable. 60 | // Implement this with `$filesystem = \MJRider\FlysystemFactory\create(getenv('STORAGE_ENDPOINT'));` 61 | $filesystemRoot = getenv('STORAGE_ENDPOINT') ?: __DIR__ . '/../tests/fixtures'; 62 | 63 | $adapter = new \League\Flysystem\Adapter\Local($filesystemRoot); 64 | 65 | $graph = new \EasyRdf_Graph(); 66 | 67 | // Create Formats objects 68 | $formats = new \Pdsinterop\Rdf\Formats(); 69 | 70 | $serverParams = $request->getServerParams(); 71 | 72 | $serverUri = ''; 73 | if (isset($serverParams['SERVER_NAME'])) { 74 | $serverUri = vsprintf("%s://%s%s", [ 75 | // FIXME: doublecheck that this is the correct url; 76 | getenv('PROXY_MODE') ? 'http' : 'https', 77 | $serverParams['SERVER_NAME'], 78 | $serverParams['REQUEST_URI'] ?? '', 79 | ]); 80 | } 81 | 82 | // Create the RDF Adapter 83 | $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( 84 | $adapter, 85 | $graph, 86 | $formats, 87 | $serverUri 88 | ); 89 | 90 | $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); 91 | 92 | $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); 93 | 94 | $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); 95 | $filesystem->addPlugin($plugin); 96 | 97 | return $filesystem; 98 | }); 99 | 100 | $container->share(\PHPTAL::class, function () { 101 | $template = new \PHPTAL(); 102 | $template->setTemplateRepository(__DIR__ . '/../src/Template'); 103 | return $template; 104 | }); 105 | 106 | $container->add(ResourceController::class, function () use ($container) { 107 | $filesystem = $container->get(FilesystemInterface::class); 108 | 109 | $server = new ResourceServer($filesystem, new Response()); 110 | 111 | $baseUrl = getenv('SERVER_ROOT') ?: "https://" . $_SERVER["SERVER_NAME"]; 112 | $pubsub = getenv('PUBSUB_URL') ?: "http://" .$_SERVER["SERVER_NAME"] . ":8080/"; 113 | $server->setBaseUrl($baseUrl); 114 | $server->setPubSubUrl($pubsub); 115 | 116 | return new ResourceController($server); 117 | }); 118 | 119 | $controllers = [ 120 | AddSlashToPathController::class, 121 | ApprovalController::class, 122 | AuthorizeController::class, 123 | CardController::class, 124 | CorsController::class, 125 | HandleApprovalController::class, 126 | HelloWorldController::class, 127 | HttpToHttpsController::class, 128 | JwksController::class, 129 | LoginController::class, 130 | OpenidController::class, 131 | ProfileController::class, 132 | RegisterController::class, 133 | StorageController::class, 134 | TokenController::class, 135 | ]; 136 | 137 | $traits = [ 138 | 'setFilesystem' => [FilesystemInterface::class], 139 | 'setResponse' => [ResponseInterface::class], 140 | 'setTemplate' => [\PHPTAL::class], 141 | ]; 142 | 143 | $traitMethods = array_keys($traits); 144 | 145 | array_walk($controllers, static function ($controller) use ($container, $traits, $traitMethods) { 146 | $definition = $container->add($controller); 147 | 148 | $methods = get_class_methods($controller); 149 | 150 | array_walk ($methods, static function ($method) use ($definition, $traitMethods, $traits) { 151 | if (in_array($method, $traitMethods, true)) { 152 | $definition->addMethodCall($method, $traits[$method]); 153 | } 154 | }); 155 | }); 156 | 157 | $strategy->setContainer($container); 158 | 159 | /*/ Default output is HTML, should return a Response object /*/ 160 | $router->setStrategy($strategy); 161 | 162 | /*/ Redirect all HTTP requests to HTTPS, unless we are behind a proxy /*/ 163 | if ( ! getenv('PROXY_MODE')) { 164 | $router->map('GET', '/{page:(?:.|/)*}', HttpToHttpsController::class)->setScheme('http'); 165 | } 166 | 167 | /*/ To prevent "405 Method Not Allowed" from the Router we only map `/*` to 168 | * OPTIONS when OPTIONS are actually requested. 169 | /*/ 170 | if ($request->getMethod() === 'OPTIONS') { 171 | $router->map('OPTIONS', '/{path:.*}', CorsController::class); 172 | } 173 | 174 | $router->map('GET', '/', HelloWorldController::class); 175 | 176 | // @FIXME: CORS handling, slash-adding (and possibly others?) should be added as middleware instead of "catchall" URLs map 177 | 178 | /*/ Create URI groups /*/ 179 | if (file_exists(__DIR__. '/favicon.ico') === false) { 180 | $router->map('GET', '/favicon.ico', static function () { 181 | return (new TextResponse( 182 | '', 183 | ))->withHeader('Content-type', 'image/svg+xml'); 184 | }); 185 | } 186 | 187 | $router->map('GET', '/login', AddSlashToPathController::class); 188 | $router->map('GET', '/profile', AddSlashToPathController::class); 189 | $router->map('GET', '/.well-known/openid-configuration', OpenidController::class); 190 | $router->map('GET', '/jwks', JwksController::class); 191 | $router->map('GET', '/login/', LoginController::class); 192 | $router->map('POST', '/login', LoginController::class); 193 | $router->map('POST', '/login/', LoginController::class); 194 | $router->map('POST', '/register', RegisterController::class); 195 | $router->map('GET', '/profile/', ProfileController::class); 196 | $router->map('GET', '/profile/card', CardController::class); 197 | $router->map('GET', '/profile/card{extension}', CardController::class); 198 | $router->map('GET', '/authorize', AuthorizeController::class); 199 | $router->map('GET', '/sharing/{clientId}/', ApprovalController::class); 200 | $router->map('POST', '/sharing/{clientId}/', HandleApprovalController::class); 201 | $router->map('POST', '/token', TokenController::class); 202 | $router->map('POST', '/token/', TokenController::class); 203 | 204 | $router->group('/storage', static function (\League\Route\RouteGroup $group) { 205 | $methods = [ 206 | 'DELETE', 207 | 'GET', 208 | 'HEAD', 209 | // 'OPTIONS', // @TODO: This breaks because of the CorsController being added to `OPTION /*` in the index.php 210 | 'PATCH', 211 | 'POST', 212 | 'PUT', 213 | ]; 214 | 215 | array_walk($methods, static function ($method) use (&$group) { 216 | $group->map($method, '/', AddSlashToPathController::class); 217 | $group->map($method, '{path:.*}', ResourceController::class); 218 | }); 219 | }); 220 | 221 | try { 222 | $response = $router->dispatch($request); 223 | } catch (HttpException $exception) { 224 | $status = $exception->getStatusCode(); 225 | 226 | $message = 'Yeah, that\'s an error.'; 227 | if ($exception instanceof NotFoundException) { 228 | $message = 'No such page.'; 229 | } 230 | 231 | $html = "

{$message}

{$exception->getMessage()} ({$status})

"; 232 | 233 | if (getenv('ENVIRONMENT') === 'development') { 234 | $html .= "
{$exception->getTraceAsString()}
"; 235 | } 236 | 237 | $response = new HtmlResponse($html, $status, $exception->getHeaders()); 238 | } catch (\Throwable $exception) { 239 | $class = get_class($exception); 240 | $html = "

Oh-no! The developers messed up!

{$exception->getMessage()} ($class)

"; 241 | 242 | if (getenv('ENVIRONMENT') === 'development') { 243 | $html .= 244 | "

{$exception->getFile()}:{$exception->getLine()}

" . 245 | "
{$exception->getTraceAsString()}
" 246 | ; 247 | } 248 | 249 | $response = new HtmlResponse($html, 500, []); 250 | } 251 | 252 | // send the response to the browser 253 | $emitter->emit($response); 254 | exit; 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Standalone PHP Solid Server 2 | 3 | [![Project stage: Development][project-stage-badge: Development]][project-stage-page] 4 | [![License][license-shield]][license-link] 5 | [![Latest Version][version-shield]][version-link] 6 | ![Maintained][maintained-shield] 7 | 8 | [![PDS Interop][pdsinterop-shield]][pdsinterop-site] 9 | [![standard-readme compliant][standard-readme-shield]][standard-readme-link] 10 | [![keep-a-changelog compliant][keep-a-changelog-shield]][keep-a-changelog-link] 11 | 12 | _Standalone Solid Server written in PHP by PDS Interop_ 13 | 14 | ## Table of Contents 15 | 16 | 17 | 18 | - [Background](#background) 19 | - [Installation](#installation) 20 | - [Usage](#usage) 21 | - [Docker images](#docker-images) 22 | - [Local environment](#local-environment) 23 | - [Built-in PHP HTTP server](#built-in-php-http-server) 24 | - [Security](#security) 25 | - [Running solid/webid-provider-tests](#running-solidwebid-provider-tests) 26 | - [Available Features](#available-features) 27 | - [Development](#development) 28 | - [Project structure](#project-structure) 29 | - [Testing](#testing) 30 | - [Contributing](#contributing) 31 | - [License](#license) 32 | 33 | 34 | 35 | ## Background 36 | 37 | The Solid specifications defines what makes a "Solid Server". Parts of 38 | those specifications are still likely to change, but at the time of this writing, 39 | they define: 40 | 41 | - Authentication 42 | - Authorization (and access control) 43 | - Content representation 44 | - Identity 45 | - Profiles 46 | - Resource (reading and writing) API 47 | - Social Web App Protocols (Notifications, Friends Lists, Followers and Following) 48 | 49 | ## Installation 50 | 51 | To install the project, clone it from GitHub and install the PHP dependencies 52 | using Composer: 53 | 54 | ```sh 55 | git clone git://github.com/pdsinterop/php-solid-server.git \ 56 | && cd php-solid-server \ 57 | && composer install --no-dev --prefer-dist 58 | ``` 59 | At this point, the application is ready to run. 60 | 61 | ## Usage 62 | 63 | The PHP Solid server can be run in several different ways. 64 | 65 | The application can be run with a Docker image of your choice or on a local 66 | environment, using Apache, NginX, or PHP's internal HTTP server. The latter is 67 | only advised in development. 68 | 69 | For security reasons, the server expects to run on HTTPS (also known as HTTP+TLS). 70 | 71 | To run insecure, for instance when the application is run behind a proxy or in a 72 | PHP-FPM (or similar) setup, set the environment variable `PROXY_MODE`. 73 | This will allow the application to accept HTTP requests. 74 | 75 | ### Docker images 76 | 77 | When running with your own Docker image, make sure to mount the project folder 78 | to wherever the image expects it to be, e.g. `/app` or `/var/www`. 79 | 80 | For instance: 81 | 82 | ``` 83 | export PORT=8080 && \ 84 | docker run \ 85 | --env "PORT=${PORT}" \ 86 | --expose "${PORT}" \ 87 | --network host \ 88 | --rm \ 89 | --volume "$PWD:/app" \ 90 | -it \ 91 | php:7.3 \ 92 | php --docroot /app/web/ --server "localhost:${PORT}" /app/web/index.php 93 | ``` 94 | Or on Mac: 95 | ``` 96 | export PORT=8080 && \ 97 | docker run \ 98 | --env "PORT=${PORT}" \ 99 | --expose "${PORT}" \ 100 | -p "${PORT}:${PORT}" \ 101 | --rm \ 102 | --volume "$PWD:/app" \ 103 | -it \ 104 | php:7.3 \ 105 | php --docroot /app/web/ --server "localhost:${PORT}" /app/web/index.php 106 | ``` 107 | 108 | 109 | ### Local environment 110 | 111 | How to run this application in an Apache, NginX, or other popular HTTP servers 112 | falls outside the scope of this project. 113 | 114 | For development purposes, the internal PHP HTTP server _is_ explained below. 115 | 116 | #### Built-in PHP HTTP server 117 | 118 | For development purposes a Composer `serve-dev` command has been provided. This will 119 | run the application using PHP internal HTTP server. 120 | 121 | To use it, run `composer serve-dev` in the project root. 122 | 123 | **!!! FOR SECURITY REASONS, DO NOT USE THIS METHOD IN PRODUCTION !!!** 124 | 125 | By default, the application is hosted on `localhost` port `8080`. 126 | So if you visit http://localhost:8080/ with your browser, you should see "Hello, World!". 127 | 128 | Both the `HOST` and `PORT` can be configured before running the command by 129 | setting them in the environment, for instance: 130 | 131 | ```sh 132 | HOST='solid.local' PORT=1234 composer serve-dev 133 | ``` 134 | 135 | This command can also be run through a docker container, for instance: 136 | 137 | ``` 138 | export PORT=8080 && \ 139 | docker run \ 140 | --env "PORT=${PORT}" \ 141 | --expose "${PORT}" \ 142 | --network host \ 143 | --rm \ 144 | --volume "$PWD:/app" \ 145 | -it \ 146 | composer:latest \ 147 | serve 148 | ``` 149 | 150 | 157 | 158 | ## Running solid/webid-provider-tests 159 | Due to https://github.com/pdsinterop/php-solid-server/issues/8 you should run, in one terminal window: 160 | ```sh 161 | HOST=127.0.0.1 composer serve-dev 162 | ``` 163 | and in another you run the [webid-provider-test](https://github.com/solid/webid-provider-tests) as: 164 | ```sh 165 | SERVER_ROOT=http://localhost:8080 ./node_modules/.bin/jest test/surface/fetch-openid-config.test.ts 166 | ``` 167 | The current `dev` branch of php-solid-server should pass roughly 7 out of 17 tests. 168 | 169 | ## Available Features 170 | 171 | Based on the specifications, the features listed below _should_ be available. 172 | 173 | The checkboxes show which features _are_, and which ones _are not_. 174 | 175 | The underlying functionality for these features is provided by: 176 | 177 | - [auth] = [`pdsinterop/solid-auth`](https://github.com/pdsinterop/php-solid-auth) 178 | - [crud] = [`pdsinterop/solid-crud`](https://github.com/pdsinterop/php-solid-crud) 179 | - [p/s] = [`pdsinterop/solid-pubsub-server`](https://github.com/pdsinterop/php-solid-pubsub-server) 180 | - [rdf] = [`pdsinterop/flysystem-rdf`](https://github.com/pdsinterop/flysystem-rdf) 181 | 182 | 1. User 183 | - [x] Authentication [auth] (since **v0.3**) 184 | - [x] Identity (since **v0.2**) 185 | - [x] Profiles (since **v0.2**) 186 | 2. Data storage 187 | - [x] Content representation [rdf] (since **v0.4**) 188 | - [x] Resource API 189 | - [x] HTTP REST API [crud] (since **v0.4**) 190 | - [x] Websocket API [p/s] (since **v0.6**) 191 | 3. Web Acces Control List 192 | - [x] Authorization (and Access Control) [crud] (since **v0.6**) 193 | 4. Social web apps 194 | - [ ] Calendar 195 | - [ ] Contacts 196 | - [ ] Friends Lists (Followers, Following) 197 | - [ ] Notifications 198 | 199 | ## Development 200 | 201 | The easiest way to develop this project is by running the environment provided 202 | by the `docker-compose.yml` file. This can be done by running `docker-compose up`. 203 | 204 | This will start the application and a pubsub server in separate docker containers. 205 | 206 | ### Project structure 207 | 208 | This project is structured as follows: 209 | 210 | ``` 211 | . 212 | ├── bin/ <- CLI scripts 213 | ├── config/ <- Empty directory where server configuration is generated 214 | ├── docs/ <- Documentation 215 | ├── src/ <- Source code 216 | ├── tests/ <- Test fixtures, Integration- and unit-tests 217 | ├── vendor/ <- Third-party and vendor code 218 | ├── web/ <- Web content 219 | ├── composer.json <- PHP package and dependency configuration 220 | └── README.md <- You are now here 221 | ``` 222 | 223 | ### Testing 224 | 225 | The PHPUnit version to be used is the one installed as a `dev-` dependency via composer. It can be run using `composer test` or by calling it directly: 226 | 227 | ```sh 228 | $ ./bin/phpunit 229 | ``` 230 | 231 | ## Contributing 232 | 233 | Questions or feedback can be given by [opening an issue on GitHub][issues-link]. 234 | 235 | All PDS Interop projects are open source and community-friendly. 236 | Any contribution is welcome! 237 | For more details read the [contribution guidelines][contributing-link]. 238 | 239 | All PDS Interop projects adhere to [the Code Manifesto](http://codemanifesto.com) 240 | as its [code-of-conduct][code-of-conduct]. Contributors are expected to abide by its terms. 241 | 242 | There is [a list of all contributors on GitHub][contributors-page]. 243 | 244 | For a list of changes see the [CHANGELOG][changelog] or [the GitHub releases page][releases-page]. 245 | 246 | ## License 247 | 248 | All code created by PDS Interop is licensed under the [MIT License][license-link]. 249 | 250 | [changelog]: CHANGELOG.md 251 | [code-of-conduct]: CODE_OF_CONDUCT.md 252 | [contributing-link]: CONTRIBUTING.md 253 | [contributors-page]: https://github.com/pdsinterop/php-solid-server/contributors 254 | [issues-link]: https://github.com/pdsinterop/php-solid-server/issues 255 | [releases-page]: https://github.com/pdsinterop/php-solid-server/releases 256 | [keep-a-changelog-link]: https://keepachangelog.com/ 257 | [keep-a-changelog-shield]: https://img.shields.io/badge/Keep%20a%20Changelog-f15d30.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHZpZXdCb3g9IjAgMCAxODcgMTg1Ij48cGF0aCBkPSJNNjIgN2MtMTUgMy0yOCAxMC0zNyAyMmExMjIgMTIyIDAgMDAtMTggOTEgNzQgNzQgMCAwMDE2IDM4YzYgOSAxNCAxNSAyNCAxOGE4OSA4OSAwIDAwMjQgNCA0NSA0NSAwIDAwNiAwbDMtMSAxMy0xYTE1OCAxNTggMCAwMDU1LTE3IDYzIDYzIDAgMDAzNS01MiAzNCAzNCAwIDAwLTEtNWMtMy0xOC05LTMzLTE5LTQ3LTEyLTE3LTI0LTI4LTM4LTM3QTg1IDg1IDAgMDA2MiA3em0zMCA4YzIwIDQgMzggMTQgNTMgMzEgMTcgMTggMjYgMzcgMjkgNTh2MTJjLTMgMTctMTMgMzAtMjggMzhhMTU1IDE1NSAwIDAxLTUzIDE2bC0xMyAyaC0xYTUxIDUxIDAgMDEtMTItMWwtMTctMmMtMTMtNC0yMy0xMi0yOS0yNy01LTEyLTgtMjQtOC0zOWExMzMgMTMzIDAgMDE4LTUwYzUtMTMgMTEtMjYgMjYtMzMgMTQtNyAyOS05IDQ1LTV6TTQwIDQ1YTk0IDk0IDAgMDAtMTcgNTQgNzUgNzUgMCAwMDYgMzJjOCAxOSAyMiAzMSA0MiAzMiAyMSAyIDQxLTIgNjAtMTRhNjAgNjAgMCAwMDIxLTE5IDUzIDUzIDAgMDA5LTI5YzAtMTYtOC0zMy0yMy01MWE0NyA0NyAwIDAwLTUtNWMtMjMtMjAtNDUtMjYtNjctMTgtMTIgNC0yMCA5LTI2IDE4em0xMDggNzZhNTAgNTAgMCAwMS0yMSAyMmMtMTcgOS0zMiAxMy00OCAxMy0xMSAwLTIxLTMtMzAtOS01LTMtOS05LTEzLTE2YTgxIDgxIDAgMDEtNi0zMiA5NCA5NCAwIDAxOC0zNSA5MCA5MCAwIDAxNi0xMmwxLTJjNS05IDEzLTEzIDIzLTE2IDE2LTUgMzItMyA1MCA5IDEzIDggMjMgMjAgMzAgMzYgNyAxNSA3IDI5IDAgNDJ6bS00My03M2MtMTctOC0zMy02LTQ2IDUtMTAgOC0xNiAyMC0xOSAzN2E1NCA1NCAwIDAwNSAzNGM3IDE1IDIwIDIzIDM3IDIyIDIyLTEgMzgtOSA0OC0yNGE0MSA0MSAwIDAwOC0yNCA0MyA0MyAwIDAwLTEtMTJjLTYtMTgtMTYtMzEtMzItMzh6bS0yMyA5MWgtMWMtNyAwLTE0LTItMjEtN2EyNyAyNyAwIDAxLTEwLTEzIDU3IDU3IDAgMDEtNC0yMCA2MyA2MyAwIDAxNi0yNWM1LTEyIDEyLTE5IDI0LTIxIDktMyAxOC0yIDI3IDIgMTQgNiAyMyAxOCAyNyAzM3MtMiAzMS0xNiA0MGMtMTEgOC0yMSAxMS0zMiAxMXptMS0zNHYxNGgtOFY2OGg4djI4bDEwLTEwaDExbC0xNCAxNSAxNyAxOEg5NnoiLz48L3N2Zz4K 258 | [license-link]: ./LICENSE 259 | [license-shield]: https://img.shields.io/github/license/pdsinterop/php-solid-server.svg 260 | [maintained-shield]: https://img.shields.io/maintenance/yes/2022.svg 261 | [pdsinterop-shield]: https://img.shields.io/badge/-PDS%20Interop-7C4DFF.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii01IC01IDExMCAxMTAiIGZpbGw9IiNGRkYiIHN0cm9rZS13aWR0aD0iMCI+CiAgICA8cGF0aCBkPSJNLTEgNTJoMTdhMzcuNSAzNC41IDAgMDAyNS41IDMxLjE1di0xMy43NWEyMC43NSAyMSAwIDAxOC41LTQwLjI1IDIwLjc1IDIxIDAgMDE4LjUgNDAuMjV2MTMuNzVhMzcgMzQuNSAwIDAwMjUuNS0zMS4xNWgxN2EyMiAyMS4xNSAwIDAxLTEwMiAweiIvPgogICAgPHBhdGggZD0iTSAxMDEgNDhhMi43NyAyLjY3IDAgMDAtMTAyIDBoIDE3YTIuOTcgMi44IDAgMDE2OCAweiIvPgo8L3N2Zz4K 262 | [pdsinterop-site]: https://pdsinterop.org/ 263 | [project-stage-badge: Development]: https://img.shields.io/badge/Project%20Stage-Development-yellowgreen.svg 264 | [project-stage-page]: https://blog.pother.ca/project-stages/ 265 | [standard-readme-link]: https://github.com/RichardLitt/standard-readme 266 | [standard-readme-shield]: https://img.shields.io/badge/-Standard%20Readme-brightgreen.svg 267 | [version-link]: https://packagist.org/packages/pdsinterop/php-solid-server 268 | [version-shield]: https://img.shields.io/github/v/release/pdsinterop/php-solid-server?sort=semver 269 | --------------------------------------------------------------------------------