diff --git a/composer.json b/composer.json index 3cc6e34..64ab040 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "michelf/php-markdown": "1.6.*", "camspiers/json-pretty": "1.0.*", "monolog/monolog": "1.*", - "emgiezet/errbit-php": "1.1.*" + "emgiezet/errbit-php": "1.1.*", + "p3k/utils": "^1.2" }, "require-dev": { "phpunit/phpunit": "*" diff --git a/composer.lock b/composer.lock index 3ffd50f..df18e92 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "743002238143ee7368e8c5f8a6783cae", + "content-hash": "5d6fecd1a81db73f245f5a818c28e8ac", "packages": [ { "name": "barnabywalters/mf-cleaner", @@ -893,6 +893,52 @@ "description": "Caterpillar is a background queue manager", "time": "2015-12-21T22:52:21+00:00" }, + { + "name": "p3k/utils", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/aaronpk/p3k-utils.git", + "reference": "ecd0ba1afca8e586f09792f7be5052e142d3cbde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aaronpk/p3k-utils/zipball/ecd0ba1afca8e586f09792f7be5052e142d3cbde", + "reference": "ecd0ba1afca8e586f09792f7be5052e142d3cbde", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.13", + "predis/predis": "1.1.*" + }, + "type": "library", + "autoload": { + "files": [ + "src/global.php", + "src/url.php", + "src/utils.php", + "src/date.php", + "src/cache.php", + "src/geo.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "https://aaronparecki.com" + } + ], + "description": "Some helpful functions used by https://p3k.io projects", + "homepage": "https://github.com/aaronpk/p3k-utils", + "time": "2018-03-28T13:44:56+00:00" + }, { "name": "pda/pheanstalk", "version": "v3.1.0", diff --git a/controllers/API.php b/controllers/API.php index fd4258a..772385e 100644 --- a/controllers/API.php +++ b/controllers/API.php @@ -18,7 +18,7 @@ class API { $response->headers->set($k, $v); } $response->headers->set('Content-Type', 'application/json'); - $response->setContent(json_encode($params, JSON_UNESCAPED_SLASHES)); + $response->setContent(json_encode($params, JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT)); return $response; } @@ -37,12 +37,31 @@ class API { } public function webmention(Request $request, Response $response) { - # Require the token parameter - if(!$token=$request->get('token')) { + + # Require the token or csrf parameter + if($csrf=$request->get('_csrf')) { + session_start(); + if($csrf != $_SESSION['_csrf']) { + return $this->respond($response, 401, [ + 'error' => 'invalid_csrf_token', + 'error_description' => 'An error occurred. Make sure you have only one tab open.', + ]); + } + } else if(!$token=$request->get('token')) { return $this->respond($response, 401, [ 'error' => 'authentication_required', 'error_description' => 'A token is required to use the API' ]); + + # Verify the token is valid + $role = ORM::for_table('roles')->where('token', $token)->find_one(); + + if(!$role) { + return $this->respond($response, 401, [ + 'error' => 'invalid_token', + 'error_description' => 'The token provided is not valid' + ]); + } } # Require source and target or target_domain parameters @@ -60,6 +79,16 @@ class API { ]); } + # Can only use source & target if no authentication(role) is set + if(!isset($role)) { + if($target_domain) { + return $this->respond($response, 400, [ + 'error' => 'unauthorized', + 'error_description' => 'Can only use the target_domain feature when providing a token from the API', + ]); + } + } + $urlregex = '/^https?:\/\/[^ ]+\.[^ ]+$/'; $domainregex = '/^[^ ]+$/'; @@ -75,7 +104,7 @@ class API { } # Don't send anything if the source domain matches the target domain - # The problem is someone pushing to Superfeedr who is also subscribed, will cause a + # The problem is someone pushing to Superfeedr who is also subscribed, will cause a # request to be sent with the source of one of their posts, and their own target domain. # This causes a whole slew of webmentions to be queued up, almost all of which are not needed. if($target_domain) { @@ -89,16 +118,6 @@ class API { } } - # Verify the token is valid - $role = ORM::for_table('roles')->where('token', $token)->find_one(); - - if(!$role) { - return $this->respond($response, 401, [ - 'error' => 'invalid_token', - 'error_description' => 'The token provided is not valid' - ]); - } - # Check the blacklist of domains that are known to not accept webmentions if($target && !Telegraph\Webmention::isProbablySupported($target)) { return $this->respond($response, 400, [ @@ -138,9 +157,11 @@ class API { $xpath = new DOMXPath($doc); $found = []; + $links = []; foreach($xpath->query('//a[@href]') as $href) { $url = $href->getAttribute('href'); if($target) { + $links[] = $url; # target parameter was provided if($url == $target) { $found[$url] = null; @@ -157,7 +178,8 @@ class API { if(!$found) { return $this->respond($response, 400, [ 'error' => 'no_link_found', - 'error_description' => 'The source document does not have a link to the target URL or domain' + 'error_description' => 'The source document does not have a link to the target URL or domain', + 'links' => $links ]); } } @@ -167,8 +189,8 @@ class API { $statusURLs = []; foreach($found as $url=>$_) { $w = ORM::for_table('webmentions')->create(); - $w->site_id = $role->site_id; - $w->created_by = $role->user_id; + $w->site_id = isset($role) ? $role->site_id : 0; + $w->created_by = isset($role) ? $role->user_id : 0; $w->created_at = date('Y-m-d H:i:s'); $w->token = self::generateStatusToken(); $w->source = $source; @@ -184,7 +206,7 @@ class API { $statusURLs[] = Config::$base . 'webmention/' . $w->token; } - if ($target) { + if($target) { $body = [ 'status' => 'queued', 'location' => $statusURLs[0] @@ -226,7 +248,7 @@ class API { $site = ORM::for_table('sites')->where('id', $role->site_id)->find_one(); if(is_array($input) - && array_key_exists('items', $input) + && array_key_exists('items', $input) && ($items = $input['items']) && is_array($items) && array_key_exists(0, $items) @@ -301,8 +323,9 @@ class API { if($status && $status->http_code) $data['http_code'] = (int)$status->http_code; - if($status && $status->raw_response) + if($status && $status->raw_response) { $data['http_body'] = $status->raw_response; + } if($summary) $data['summary'] = $summary; diff --git a/controllers/Controller.php b/controllers/Controller.php index 47df9fa..2509850 100644 --- a/controllers/Controller.php +++ b/controllers/Controller.php @@ -41,8 +41,11 @@ class Controller { } public function index(Request $request, Response $response) { + p3k\session_setup(); + $response->setContent(view('index', [ - 'title' => 'Telegraph' + 'title' => 'Telegraph', + 'user' => $this->_user(), ])); return $response; } @@ -150,6 +153,20 @@ class Controller { return $response; } + public function send_a_webmention(Request $request, Response $response) { + p3k\session_setup(); + + $_SESSION['_csrf'] = random_string(16); + + $response->setContent(view('send-a-webmention', [ + 'title' => 'Send a Webmention with Telegraph', + 'user' => $this->_user(), + 'accounts' => $this->_accounts(), + 'csrf' => $_SESSION['_csrf'], + ])); + return $response; + } + public function new_site(Request $request, Response $response) { if(!$this->_is_logged_in($request, $response)) { return $response; @@ -173,7 +190,7 @@ class Controller { 'role' => $role, 'site' => $site ])); - return $response; + return $response; } public function save_site(Request $request, Response $response) { @@ -341,6 +358,7 @@ class Controller { } private function _accounts() { + if(!session('user_id')) return []; return ORM::for_table('sites')->join('roles', 'roles.site_id = sites.id') ->where('roles.user_id', session('user_id')) ->find_many(); diff --git a/public/index.php b/public/index.php index d2b50d1..ae28022 100644 --- a/public/index.php +++ b/public/index.php @@ -24,6 +24,7 @@ $templates = new League\Plates\Engine(dirname(__FILE__).'/../views'); $router->addRoute('GET', '/', 'Controller::index'); $router->addRoute('GET', '/dashboard', 'Controller::dashboard'); +$router->addRoute('GET', '/send-a-webmention', 'Controller::send_a_webmention'); $router->addRoute('GET', '/site/new', 'Controller::new_site'); $router->addRoute('GET', '/site/edit', 'Controller::new_site'); $router->addRoute('POST', '/site/save', 'Controller::save_site'); diff --git a/views/footer-block.php b/views/footer-block.php index a88a38a..02441cc 100644 --- a/views/footer-block.php +++ b/views/footer-block.php @@ -4,6 +4,7 @@

Telegraph

@@ -189,7 +193,11 @@ $menu = [
- Log in + + Dashboard + + Log in +
diff --git a/views/send-a-webmention.php b/views/send-a-webmention.php new file mode 100644 index 0000000..29bba6d --- /dev/null +++ b/views/send-a-webmention.php @@ -0,0 +1,57 @@ +layout('layout-loggedin', ['title' => $title, 'user' => $user, 'accounts' => $accounts]); ?> + +
+ +

Send a Webmention

+ +
+
+
+
+
+
+ +
+
+ +
+

Enter a source URL (your post) and target URL (the post you linked to).

+

Telegraph will discover the Webmention endpoint of the target URL and send the Webmention for you.

+

You'll be able to see the progress after you click "send".

+
+ +
+ + diff --git a/views/webmention-details.php b/views/webmention-details.php index 3b9bc47..f3af2db 100644 --- a/views/webmention-details.php +++ b/views/webmention-details.php @@ -53,7 +53,7 @@

Details

-

The request is queued for processing. Check for updates again later.

+

The request is queued for processing. Refresh this page for updates.