diff --git a/README.md b/README.md index 0aeb8e6..090404f 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,5 @@ A callback from Telegraph will include the following post body parameters: ## Credits Telegraph photo: https://www.flickr.com/photos/nostri-imago/3407786186 + +Telegraph icon: https://thenounproject.com/search/?q=telegraph&i=22058 diff --git a/composer.json b/composer.json index b85bc10..6bd3bd4 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,8 @@ "php": ">=5.5", "mf2/mf2": "0.2.*", "indieweb/mention-client": "1.*", + "indieauth/client": "0.1.*", + "firebase/php-jwt": "~3.0", "league/route": "~1.2", "league/plates": "~3.1" }, @@ -13,7 +15,9 @@ "files": [ "config.php", "lib/helpers.php", - "controllers/Controller.php" + "controllers/Controller.php", + "controllers/Auth.php", + "controllers/API.php" ] } } diff --git a/composer.lock b/composer.lock index 0921ad2..d64b131 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,177 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "5cc37d4bc843c6cbd92dc660ef9e71d8", - "content-hash": "07f934dcf7d5e224f6c980c51525aa81", + "hash": "5630557b773b8342de2ebfcfbe23f013", + "content-hash": "88dfc4a35925d318e92d4881b37d70a0", "packages": [ + { + "name": "barnabywalters/mf-cleaner", + "version": "v0.1.4", + "source": { + "type": "git", + "url": "https://github.com/barnabywalters/php-mf-cleaner.git", + "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4", + "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4", + "shasum": "" + }, + "require-dev": { + "php": ">=5.3", + "phpunit/phpunit": "*" + }, + "suggest": { + "mf2/mf2": "To parse microformats2 structures from (X)HTML" + }, + "type": "library", + "autoload": { + "files": [ + "src/BarnabyWalters/Mf2/Functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barnaby Walters", + "email": "barnaby@waterpigs.co.uk" + } + ], + "description": "Cleans up microformats2 array structures", + "time": "2014-10-06 23:11:15" + }, + { + "name": "firebase/php-jwt", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", + "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2015-07-22 18:31:08" + }, + { + "name": "indieauth/client", + "version": "0.1.11", + "source": { + "type": "git", + "url": "https://github.com/indieweb/indieauth-client-php.git", + "reference": "6504ed0d4714084e9955f639d6e5cf4e976f9038" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/indieauth-client-php/zipball/6504ed0d4714084e9955f639d6e5cf4e976f9038", + "reference": "6504ed0d4714084e9955f639d6e5cf4e976f9038", + "shasum": "" + }, + "require": { + "barnabywalters/mf-cleaner": "0.*", + "indieweb/link-rel-parser": "0.1.1", + "mf2/mf2": "0.2.*", + "php": ">5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "IndieAuth": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache 2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "http://aaronparecki.com" + } + ], + "description": "IndieAuth Client Library", + "time": "2015-08-30 22:29:40" + }, + { + "name": "indieweb/link-rel-parser", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/indieweb/link-rel-parser-php.git", + "reference": "9e0e635fd301a8b1da7bc181f651f029c531dbb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/link-rel-parser-php/zipball/9e0e635fd301a8b1da7bc181f651f029c531dbb6", + "reference": "9e0e635fd301a8b1da7bc181f651f029c531dbb6", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/IndieWeb/link_rel_parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "http://aaronparecki.com" + }, + { + "name": "Tantek Çelik", + "homepage": "http://tantek.com" + } + ], + "description": "Parse rel values from HTTP headers", + "homepage": "https://github.com/indieweb/link-rel-parser-php", + "keywords": [ + "http", + "indieweb", + "microformats2" + ], + "time": "2013-12-23 00:14:58" + }, { "name": "indieweb/mention-client", "version": "1.0.0", diff --git a/controllers/API.php b/controllers/API.php new file mode 100644 index 0000000..8794760 --- /dev/null +++ b/controllers/API.php @@ -0,0 +1,9 @@ +setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'return_to' => $request->get('return_to') + ])); + return $response; + } + + public function login_start(Request $request, Response $response) { + + if(!$request->get('url') || !($me = IndieAuth\Client::normalizeMeURL($request->get('url')))) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid URL', + 'error_description' => 'The URL you entered, "' . htmlspecialchars($request->get('url')) . '" is not valid.' + ])); + return $response; + } + + $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($me); + + $state = JWT::encode([ + 'me' => $me, + 'authorization_endpoint' => $authorizationEndpoint, + 'return_to' => $request->get('return_to'), + 'time' => time(), + 'exp' => time()+300 // verified by the JWT library + ], Config::$secretKey); + + if($authorizationEndpoint) { + // If the user specified only an authorization endpoint, use that + $authorizationURL = IndieAuth\Client::buildAuthorizationURL($authorizationEndpoint, $me, self::_buildRedirectURI(), Config::$clientID, $state); + } else { + // Otherwise, fall back to indieauth.com + $authorizationURL = IndieAuth\Client::buildAuthorizationURL(Config::$defaultAuthorizationEndpoint, $me, self::_buildRedirectURI(), Config::$clientID, $state); + } + + $response->setStatusCode(302); + $response->headers->set('Location', $authorizationURL); + return $response; + } + + public function login_callback(Request $request, Response $response) { + + if(!$request->get('state') || !$request->get('code') || !$request->get('me')) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Missing Parameters', + 'error_description' => 'The auth server did not return the necessary parameters, state and code and me.' + ])); + return $response; + } + + // Validate the "state" parameter to ensure this request originated at this client + try { + $state = JWT::decode($request->get('state'), Config::$secretKey, ['HS256']); + + if(!$state) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid State', + 'error_description' => 'The state parameter was not valid.' + ])); + return $response; + } + } catch(Exception $e) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid State', + 'error_description' => 'The state parameter was invalid:
'.htmlspecialchars($e->getMessage()) + ])); + return $response; + } + + // Discover the authorization endpoint from the "me" that was returned by the auth server + // This allows the auth server to return a different URL than the user originally entered, + // for example if the user enters multiusersite.example the auth server can return multiusersite.example/alice + if($state->authorization_endpoint) { // only discover the auth endpoint if one was originally found, otherwise use our fallback + $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($request->get('me')); + } else { + $authorizationEndpoint = Config::$defaultAuthorizationEndpoint; + } + + // Verify the code with the auth server + $token = IndieAuth\Client::verifyIndieAuthCode($authorizationEndpoint, $request->get('code'), $request->get('me'), self::_buildRedirectURI(), Config::$clientID, $request->get('state'), true); + + if(!array_key_exists('auth', $token) || !array_key_exists('me', $token['auth'])) { + // The auth server didn't return a "me" URL + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid Auth Server Response', + 'error_description' => 'The authorization server did not return a valid response:
'.htmlspecialchars(json_encode($token)) + ])); + return $response; + } + + // Create or load the user + + session_start(); + $_SESSION['me'] = $token['auth']['me']; + $response->setStatusCode(302); + $response->headers->set('Location', ($state->return_to ?: '/dashboard')); + return $response; + } + + private static function _buildRedirectURI() { + return 'http' . (Config::$ssl ? 's' : '') . '://' . $_SERVER['SERVER_NAME'] . '/login/callback'; + } + +} diff --git a/controllers/Controller.php b/controllers/Controller.php index 292caf2..5714f1c 100644 --- a/controllers/Controller.php +++ b/controllers/Controller.php @@ -4,6 +4,18 @@ use Symfony\Component\HttpFoundation\Response; class Controller { + private function _is_logged_in(&$request, &$response) { + session_start(); + if(!array_key_exists('me', $_SESSION)) { + session_destroy(); + $response->setStatusCode(302); + $response->headers->set('Location', '/login?return_to='.$request->getPathInfo()); + return false; + } else { + return true; + } + } + public function index(Request $request, Response $response) { $response->setContent(view('index', [ 'title' => 'Telegraph' @@ -11,4 +23,15 @@ class Controller { return $response; } + public function dashboard(Request $request, Response $response) { + if(!$this->_is_logged_in($request, $response)) { + return $response; + } + + $response->setContent(view('dashboard', [ + 'title' => 'Dashboard' + ])); + return $response; + } + } diff --git a/public/assets/telegraph-logo-256.png b/public/assets/telegraph-logo-256.png new file mode 100644 index 0000000..fcc1a63 Binary files /dev/null and b/public/assets/telegraph-logo-256.png differ diff --git a/public/index.php b/public/index.php index 710eab1..fc810dd 100644 --- a/public/index.php +++ b/public/index.php @@ -8,6 +8,15 @@ $router = new League\Route\RouteCollection; $templates = new League\Plates\Engine(dirname(__FILE__).'/../views'); $router->addRoute('GET', '/', 'Controller::index'); +$router->addRoute('GET', '/dashboard', 'Controller::dashboard'); +$router->addRoute('GET', '/api', 'Controller::api'); + +$router->addRoute('POST', '/webmention', 'API::webmention'); +$router->addRoute('GET', '/webmention/{code}', 'API::webmention_status'); + +$router->addRoute('GET', '/login', 'Auth::login'); +$router->addRoute('POST', '/login/start', 'Auth::login_start'); +$router->addRoute('GET', '/login/callback', 'Auth::login_callback'); diff --git a/views/dashboard.php b/views/dashboard.php new file mode 100644 index 0000000..2ca7e67 --- /dev/null +++ b/views/dashboard.php @@ -0,0 +1,3 @@ +layout('layout', ['title' => $title]); ?> + + diff --git a/views/index.php b/views/index.php index 153bb49..8d5485f 100644 --- a/views/index.php +++ b/views/index.php @@ -153,7 +153,7 @@ $menu = [ @@ -164,7 +164,7 @@ $menu = [ $name): ?> - Login + Login @@ -191,7 +191,7 @@ $menu = [ Telegraph

Easily send Webmentions from your website

-
Get Started
+ Get Started @@ -201,7 +201,7 @@ $menu = [

We send webmentions for you

-

Let Telegraph send webmentions for you. With a simple API, Telegraph will handle sending webmentions to other websites. Let Telegraph handle webmention discovery, and retrying on temporary failures. Telegraph will notify your site when a webmention was successfully sent.

+

Let Telegraph send webmentions for you. With a simple API, Telegraph will handle sending webmentions to other websites. Let us handle webmention discovery, and retrying on temporary failures. Telegraph will notify your site when a webmention was successfully sent.

Send webmentions automatically

You can even let Telegraph subscribe to your feed, and it will send webmentions whenever you publish a new post.

diff --git a/views/layout.php b/views/layout.php new file mode 100644 index 0000000..95fbe89 --- /dev/null +++ b/views/layout.php @@ -0,0 +1,22 @@ + + + + + + + + <?= $this->e($title) ?> + + + + + + + +section('content') ?> + + + + + diff --git a/views/login.php b/views/login.php new file mode 100644 index 0000000..1b4202b --- /dev/null +++ b/views/login.php @@ -0,0 +1,54 @@ +layout('layout', ['title' => $title]); ?> + + + +
+
+

+ +
+ Sign in to Telegraph +
+

+ + +
+
+ +
+ + +
+
+
+
+ + +
+
+ + +
+ +
+ +
+ +
+ What's this? About IndieAuth +
+
+