Merge branch 'release/0.5'
This commit is contained in:
commit
a7117b3972
55 changed files with 2316 additions and 1223 deletions
|
|
@ -22,7 +22,7 @@ REDIS_PASSWORD=null
|
|||
REDIS_PORT=6379
|
||||
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_HOST=mailtrap.io
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
|
|
@ -52,3 +52,5 @@ TWITTER_ACCESS_TOKEN_SECRET=
|
|||
SCOUT_DRIVER=pgsql
|
||||
|
||||
PIWIK=false
|
||||
|
||||
PSYSH_CONFIG=tinker.config.php
|
||||
|
|
|
|||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1,3 +1,4 @@
|
|||
* text=auto
|
||||
*.css linguist-vendored
|
||||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ addons:
|
|||
paths:
|
||||
- $(ls tests/Browser/screenshots/*.png | tr "\n" ":")
|
||||
- $(ls tests/Browser/console/*.log | tr "\n" ":")
|
||||
- $(ls storage/logs/*.log | tr "\n" ":")
|
||||
- $(ls /tmp/*.log | tr "\n" ":")
|
||||
|
||||
services:
|
||||
|
|
@ -49,16 +50,18 @@ install:
|
|||
- travis/install-nginx.sh
|
||||
|
||||
before_script:
|
||||
- echo 'error_log = "/tmp/php.error.log"' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
|
||||
- psql -U travis -c 'create database travis_ci_test'
|
||||
- psql -U travis -d travis_ci_test -c 'create extension postgis'
|
||||
- cp .env.travis .env
|
||||
- php artisan key:generate
|
||||
- php artisan migrate
|
||||
- php artisan db:seed
|
||||
- php artisan token:generate
|
||||
- phantomjs --webdriver=127.0.0.1:9515 --webdriver-loglevel=DEBUG &
|
||||
- sleep 5 # Give artisan some time to start serving
|
||||
|
||||
script:
|
||||
- php vendor/bin/phpunit --coverage-text
|
||||
- php artisan dusk
|
||||
- php artisan security:check
|
||||
- php vendor/bin/security-checker security:check ./composer.lock --end-point=http://security.sensiolabs.org/check_lock
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\IndieWebUser;
|
||||
use App\Services\TokenService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class GenerateToken extends Command
|
||||
{
|
||||
|
|
@ -49,10 +49,12 @@ class GenerateToken extends Command
|
|||
$data = [
|
||||
'me' => config('app.url'),
|
||||
'client_id' => route('micropub-client'),
|
||||
'scope' => 'post',
|
||||
'scope' => 'create update',
|
||||
];
|
||||
$token = $tokenService->getNewToken($data);
|
||||
Storage::disk('local')->put('dev-token', $token);
|
||||
$user = IndieWebUser::where('me', config('app.url'))->first();
|
||||
$user->token = $token;
|
||||
$user->save();
|
||||
|
||||
$this->info('Set token');
|
||||
}
|
||||
|
|
|
|||
13
app/Exceptions/InvalidTokenException.php
Normal file
13
app/Exceptions/InvalidTokenException.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidTokenException extends Exception
|
||||
{
|
||||
public function __construct($message, $code = 0, Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,35 +2,26 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\IndieWebUser;
|
||||
use IndieAuth\Client;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\TokenService;
|
||||
use App\Services\IndieAuthService;
|
||||
|
||||
class IndieAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* This service isolates the IndieAuth Client code.
|
||||
* The IndieAuth Client.
|
||||
*/
|
||||
protected $indieAuthService;
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* The Token handling service.
|
||||
*/
|
||||
protected $tokenService;
|
||||
|
||||
/**
|
||||
* Inject the dependencies.
|
||||
* Inject the dependency.
|
||||
*
|
||||
* @param \App\Services\IndieAuthService $indieAuthService
|
||||
* @param \App\Services\TokenService $tokenService
|
||||
* @param \IndieAuth\Client $client
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
IndieAuthService $indieAuthService = null,
|
||||
TokenService $tokenService = null
|
||||
) {
|
||||
$this->indieAuthService = $indieAuthService ?? new IndieAuthService();
|
||||
$this->tokenService = $tokenService ?? new TokenService();
|
||||
public function __construct(Client $client = null)
|
||||
{
|
||||
$this->client = $client ?? new Client();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,25 +35,31 @@ class IndieAuthController extends Controller
|
|||
*/
|
||||
public function start(Request $request)
|
||||
{
|
||||
$authorizationEndpoint = $this->indieAuthService->getAuthorizationEndpoint(
|
||||
$request->input('me')
|
||||
);
|
||||
if ($authorizationEndpoint !== null) {
|
||||
$authorizationURL = $this->indieAuthService->buildAuthorizationURL(
|
||||
$url = normalize_url($request->input('me'));
|
||||
$authorizationEndpoint = $this->client->discoverAuthorizationEndpoint($url);
|
||||
if ($authorizationEndpoint != null) {
|
||||
$state = bin2hex(openssl_random_pseudo_bytes(16));
|
||||
session(['state' => $state]);
|
||||
$authorizationURL = $this->client->buildAuthorizationURL(
|
||||
$authorizationEndpoint,
|
||||
$request->input('me')
|
||||
$url,
|
||||
route('indieauth-callback'), //redirect_uri
|
||||
route('micropub-client'), //client_id
|
||||
$state
|
||||
);
|
||||
if ($authorizationURL) {
|
||||
return redirect($authorizationURL);
|
||||
}
|
||||
|
||||
return redirect(route('micropub-client'))->with('error', 'Error building authorization URL');
|
||||
}
|
||||
|
||||
return redirect(route('micropub-client'))->with('error', 'Unable to determine authorisation endpoint');
|
||||
}
|
||||
|
||||
/**
|
||||
* Once they have verified themselves through the authorisation endpint
|
||||
* the next step is retreiveing a token from the token endpoint.
|
||||
* Once they have verified themselves through the authorisation endpoint
|
||||
* the next step is register/login the user.
|
||||
*
|
||||
* @param \Illuminate\Http\Rrequest $request
|
||||
* @return \Illuminate\Routing\RedirectResponse redirect
|
||||
|
|
@ -75,38 +72,16 @@ class IndieAuthController extends Controller
|
|||
'Invalid <code>state</code> value returned from indieauth server'
|
||||
);
|
||||
}
|
||||
$tokenEndpoint = $this->indieAuthService->getTokenEndpoint($request->input('me'));
|
||||
if ($tokenEndpoint === false) {
|
||||
return redirect(route('micropub-client'))->with(
|
||||
'error',
|
||||
'Unable to determine token endpoint'
|
||||
);
|
||||
}
|
||||
$data = [
|
||||
'endpoint' => $tokenEndpoint,
|
||||
'code' => $request->input('code'),
|
||||
'me' => $request->input('me'),
|
||||
'redirect_url' => route('indieauth-callback'),
|
||||
'client_id' => route('micropub-client'),
|
||||
'state' => $request->input('state'),
|
||||
];
|
||||
$token = $this->indieAuthService->getAccessToken($data);
|
||||
|
||||
if (array_key_exists('access_token', $token)) {
|
||||
$request->session()->put('me', $token['me']);
|
||||
$request->session()->put('token', $token['access_token']);
|
||||
$url = normalize_url($request->input('me'));
|
||||
$indiewebUser = IndieWebUser::firstOrCreate(['me' => $url]);
|
||||
$request->session()->put(['me' => $url]);
|
||||
|
||||
return redirect(route('micropub-client'));
|
||||
}
|
||||
|
||||
return redirect(route('micropub-client'))->with(
|
||||
'error',
|
||||
'Unable to get a token from the endpoint'
|
||||
);
|
||||
return redirect(route('micropub-client'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out the user, flush an session data, and overwrite any cookie data.
|
||||
* Log out the user, flush the session data.
|
||||
*
|
||||
* @return \Illuminate\Routing\RedirectResponse redirect
|
||||
*/
|
||||
|
|
@ -114,44 +89,6 @@ class IndieAuthController extends Controller
|
|||
{
|
||||
$request->session()->flush();
|
||||
|
||||
return redirect(route('micropub-client'))->cookie('me', 'loggedout', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user has auth’d via IndieAuth, issue a valid token.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function tokenEndpoint(Request $request)
|
||||
{
|
||||
$authData = [
|
||||
'code' => $request->input('code'),
|
||||
'me' => $request->input('me'),
|
||||
'redirect_url' => $request->input('redirect_uri'),
|
||||
'client_id' => $request->input('client_id'),
|
||||
'state' => $request->input('state'),
|
||||
];
|
||||
$auth = $this->indieAuthService->verifyIndieAuthCode($authData);
|
||||
if (array_key_exists('me', $auth)) {
|
||||
$scope = $auth['scope'] ?? '';
|
||||
$tokenData = [
|
||||
'me' => $request->input('me'),
|
||||
'client_id' => $request->input('client_id'),
|
||||
'scope' => $auth['scope'],
|
||||
];
|
||||
$token = $this->tokenService->getNewToken($tokenData);
|
||||
$content = http_build_query([
|
||||
'me' => $request->input('me'),
|
||||
'scope' => $scope,
|
||||
'access_token' => $token,
|
||||
]);
|
||||
|
||||
return response($content)
|
||||
->header('Content-Type', 'application/x-www-form-urlencoded');
|
||||
}
|
||||
$content = 'There was an error verifying the authorisation code.';
|
||||
|
||||
return response($content, 400);
|
||||
return redirect(route('micropub-client'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\IndieAuthService;
|
||||
use App\IndieWebUser;
|
||||
use IndieAuth\Client as IndieClient;
|
||||
use GuzzleHttp\Client as GuzzleClient;
|
||||
use Illuminate\Http\{Request, Response};
|
||||
|
|
@ -10,20 +10,13 @@ use GuzzleHttp\Exception\{ClientException, ServerException};
|
|||
|
||||
class MicropubClientController extends Controller
|
||||
{
|
||||
/**
|
||||
* The IndieAuth service container.
|
||||
*/
|
||||
protected $indieAuthService;
|
||||
|
||||
/**
|
||||
* Inject the dependencies.
|
||||
*/
|
||||
public function __construct(
|
||||
IndieAuthService $indieAuthService = null,
|
||||
IndieClient $indieClient = null,
|
||||
GuzzleClient $guzzleClient = null
|
||||
) {
|
||||
$this->indieAuthService = $indieAuthService ?? new IndieAuthService();
|
||||
$this->guzzleClient = $guzzleClient ?? new GuzzleClient();
|
||||
$this->indieClient = $indieClient ?? new IndieClient();
|
||||
}
|
||||
|
|
@ -37,8 +30,11 @@ class MicropubClientController extends Controller
|
|||
public function create(Request $request)
|
||||
{
|
||||
$url = $request->session()->get('me');
|
||||
$syndication = $request->session()->get('syndication');
|
||||
$mediaEndpoint = $request->session()->get('media-endpoint');
|
||||
if ($url) {
|
||||
$indiewebUser = IndieWebUser::where('me', $url)->first();
|
||||
}
|
||||
$syndication = $this->parseSyndicationTargets($indiewebUser->syndication);
|
||||
$mediaEndpoint = $indiewebUser->mediaEndpoint ?? null;
|
||||
$mediaURLs = $request->session()->get('media-links');
|
||||
|
||||
return view('micropub.create', compact('url', 'syndication', 'mediaEndpoint', 'mediaURLs'));
|
||||
|
|
@ -56,19 +52,17 @@ class MicropubClientController extends Controller
|
|||
return back();
|
||||
}
|
||||
|
||||
$mediaEndpoint = $request->session()->get('media-endpoint');
|
||||
if ($mediaEndpoint == null) {
|
||||
$user = IndieWebUser::where('me', $request->session()->get('me'))->firstOrFail();
|
||||
if ($user->mediaEndpoint == null || $user->token == null) {
|
||||
return back();
|
||||
}
|
||||
|
||||
$token = $request->session()->get('token');
|
||||
|
||||
$mediaURLs = [];
|
||||
foreach ($request->file('file') as $file) {
|
||||
try {
|
||||
$response = $this->guzzleClient->request('POST', $mediaEndpoint, [
|
||||
$response = $this->guzzleClient->request('POST', $user->mediaEndpoint, [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Authorization' => 'Bearer ' . $user->token,
|
||||
],
|
||||
'multipart' => [
|
||||
[
|
||||
|
|
@ -109,27 +103,142 @@ class MicropubClientController extends Controller
|
|||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$domain = $request->session()->get('me');
|
||||
$token = $request->session()->get('token');
|
||||
$url = normalize_url($request->session()->get('me'));
|
||||
$user = IndieWebUser::where('me', $url)->firstOrFail();
|
||||
|
||||
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint(
|
||||
$domain,
|
||||
$this->indieClient
|
||||
);
|
||||
if ($user->token == null) {
|
||||
return redirect(route('micropub-client'))->with('error', 'You haven’t requested a token yet');
|
||||
}
|
||||
|
||||
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
|
||||
if (! $micropubEndpoint) {
|
||||
return redirect(route('micropub-client'))->with('error', 'Unable to determine micropub API endpoint');
|
||||
}
|
||||
|
||||
$response = $this->postNoteRequest($request, $micropubEndpoint, $token);
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $user->token,
|
||||
];
|
||||
|
||||
if ($response->getStatusCode() == 201) {
|
||||
$request->session()->forget('media-links');
|
||||
$location = $response->getHeader('Location');
|
||||
if (is_array($location)) {
|
||||
return redirect($location[0]);
|
||||
if ($user->syntax == 'html') {
|
||||
$multipart = [
|
||||
[
|
||||
'name' => 'h',
|
||||
'contents' => 'entry',
|
||||
],
|
||||
[
|
||||
'name' => 'content',
|
||||
'contents' => $request->input('content'),
|
||||
],
|
||||
];
|
||||
if ($request->hasFile('photo')) {
|
||||
$photos = $request->file('photo');
|
||||
foreach ($photos as $photo) {
|
||||
$multipart[] = [
|
||||
'name' => 'photo[]',
|
||||
'contents' => fopen($photo->path(), 'r'),
|
||||
'filename' => $photo->getClientOriginalName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($request->input('in-reply-to') != '') {
|
||||
$multipart[] = [
|
||||
'name' => 'in-reply-to',
|
||||
'contents' => $request->input('in-reply-to'),
|
||||
];
|
||||
}
|
||||
if ($request->input('mp-syndicate-to')) {
|
||||
foreach ($request->input('mp-syndicate-to') as $syn) {
|
||||
$multipart[] = [
|
||||
'name' => 'mp-syndicate-to[]',
|
||||
'contents' => $syn,
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($request->input('location')) {
|
||||
if ($request->input('location') !== 'no-location') {
|
||||
$multipart[] = [
|
||||
'name' => 'location',
|
||||
'contents' => $request->input('location'),
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($request->input('media')) {
|
||||
foreach ($request->input('media') as $media) {
|
||||
$multipart[] = [
|
||||
'name' => 'photo[]',
|
||||
'contents' => $media,
|
||||
];
|
||||
}
|
||||
}
|
||||
try {
|
||||
$response = $this->guzzleClient->post($micropubEndpoint, [
|
||||
'multipart' => $multipart,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||
return redirect(route('micropub-client'))->with(
|
||||
'error',
|
||||
'There was a bad response from the micropub endpoint.'
|
||||
);
|
||||
}
|
||||
|
||||
return redirect($location);
|
||||
if ($response->getStatusCode() == 201) {
|
||||
$request->session()->forget('media-links');
|
||||
$location = $response->getHeader('Location');
|
||||
if (is_array($location)) {
|
||||
return redirect($location[0]);
|
||||
}
|
||||
|
||||
return redirect($location);
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->syntax == 'json') {
|
||||
$json = [];
|
||||
$json['type'] = ['h-entry'];
|
||||
$json['properties'] = ['content' => [$request->input('content')]];
|
||||
|
||||
if ($request->input('in-reply-to') != '') {
|
||||
$json['properties']['in-reply-to'] = [$request->input('in-reply-to')];
|
||||
}
|
||||
if ($request->input('mp-syndicate-to')) {
|
||||
foreach ($request->input('mp-syndicate-to') as $syn) {
|
||||
$json['properties']['mp-syndicate-to'] = [$syn];
|
||||
}
|
||||
}
|
||||
if ($request->input('location')) {
|
||||
if ($request->input('location') !== 'no-location') {
|
||||
$json['properties']['location'] = [$request->input('location')];
|
||||
}
|
||||
}
|
||||
if ($request->input('media')) {
|
||||
$json['properties']['photo'] = [];
|
||||
foreach ($request->input('media') as $media) {
|
||||
$json['properties']['photo'][] = $media;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->guzzleClient->post($micropubEndpoint, [
|
||||
'json' => $json,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||
return redirect(route('micropub-client'))->with(
|
||||
'error',
|
||||
'There was a bad response from the micropub endpoint.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() == 201) {
|
||||
$request->session()->forget('media-links');
|
||||
$location = $response->getHeader('Location');
|
||||
if (is_array($location)) {
|
||||
return redirect($location[0]);
|
||||
}
|
||||
|
||||
return redirect($location);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect(route('micropub-client'))->with('error', 'Endpoint didn’t create the note.');
|
||||
|
|
@ -143,26 +252,100 @@ class MicropubClientController extends Controller
|
|||
*/
|
||||
public function config(Request $request)
|
||||
{
|
||||
$data['me'] = $request->session()->get('me');
|
||||
$data['token'] = $request->session()->get('token');
|
||||
$data['syndication'] = $request->session()->get('syndication') ?? 'none defined';
|
||||
$data['media-endpoint'] = $request->session()->get('media-endpoint') ?? 'none defined';
|
||||
//default values
|
||||
$data = [
|
||||
'me' => '',
|
||||
'token' => 'none',
|
||||
'syndication' => 'none defined',
|
||||
'media-endpoint' => 'none defined',
|
||||
'syntax' => 'html',
|
||||
];
|
||||
if ($request->session()->has('me')) {
|
||||
$data['me'] = normalize_url($request->session()->get('me'));
|
||||
$user = IndieWebUser::where('me', $request->session()->get('me'))->first();
|
||||
$data['token'] = $user->token ?? 'none defined';
|
||||
$data['syndication'] = $user->syndication ?? 'none defined';
|
||||
$data['media-endpoint'] = $user->mediaEndpoint ?? 'none defined';
|
||||
$data['syntax'] = $user->syntax;
|
||||
}
|
||||
|
||||
return view('micropub.config', compact('data'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the micropub endpoint and store response in the session.
|
||||
* Get a new token.
|
||||
*
|
||||
* @param Illuminate\Http\Request $request
|
||||
* @return view
|
||||
*/
|
||||
public function getNewToken(Request $request)
|
||||
{
|
||||
if ($request->session()->has('me')) {
|
||||
$url = normalize_url($request->session()->get('me'));
|
||||
$authozationEndpoint = $this->indieClient->discoverAuthorizationEndpoint($url);
|
||||
if ($authozationEndpoint) {
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$request->session()->put('state', $state);
|
||||
$authorizationURL = $this->indieClient->buildAuthorizationURL(
|
||||
$authozationEndpoint,
|
||||
$url,
|
||||
route('micropub-client-get-new-token-callback'), // redirect_uri
|
||||
route('micropub-client'), //client_id
|
||||
$state,
|
||||
'create update' // scope needs to be a setting
|
||||
);
|
||||
|
||||
return redirect($authorizationURL);
|
||||
}
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback for getting a token.
|
||||
*/
|
||||
public function getNewTokenCallback(Request $request)
|
||||
{
|
||||
if ($request->input('state') !== $request->session()->get('state')) {
|
||||
return route('micropub-client')->with('error', 'The <code>state</code> didn’t match.');
|
||||
}
|
||||
$tokenEndpoint = $this->indieClient->discoverTokenEndpoint(normalize_url($request->input('me')));
|
||||
if ($tokenEndpoint) {
|
||||
$token = $this->indieClient->getAccessToken(
|
||||
$tokenEndpoint,
|
||||
$request->input('code'),
|
||||
$request->input('me'),
|
||||
route('micropub-client-get-new-token-callback'), // redirect_uri
|
||||
route('micropub-client'), // client_id
|
||||
$request->input('state')
|
||||
);
|
||||
if (array_key_exists('access_token', $token)) {
|
||||
$url = normalize_url($token['me']);
|
||||
$user = IndieWebUser::where('me', $url)->firstOrFail();
|
||||
$user->token = $token['access_token'];
|
||||
$user->save();
|
||||
|
||||
return redirect('micropub-config');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the micropub endpoint and store response.
|
||||
*
|
||||
* @param Illuminate\Http\Request $request
|
||||
* @return redirect
|
||||
*/
|
||||
public function queryEndpoint(Request $request)
|
||||
{
|
||||
$domain = $request->session()->get('me');
|
||||
$token = $request->session()->get('token');
|
||||
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint($domain);
|
||||
if ($micropubEndpoint !== null) {
|
||||
$url = normalize_url($request->session()->get('me'));
|
||||
$user = IndieWebUser::where('me', $url)->firstOrFail();
|
||||
$token = $user->token;
|
||||
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
|
||||
if ($micropubEndpoint) {
|
||||
try {
|
||||
$response = $this->guzzleClient->get($micropubEndpoint, [
|
||||
'headers' => ['Authorization' => 'Bearer ' . $token],
|
||||
|
|
@ -172,96 +355,35 @@ class MicropubClientController extends Controller
|
|||
return back();
|
||||
}
|
||||
$body = (string) $response->getBody();
|
||||
$data = json_decode($body, true);
|
||||
|
||||
$syndication = $this->parseSyndicationTargets($body);
|
||||
$request->session()->put('syndication', $syndication);
|
||||
if (array_key_exists('syndicate-to', $data)) {
|
||||
$user->syndication = json_encode($data['syndicate-to']);
|
||||
}
|
||||
|
||||
$mediaEndpoint = $this->parseMediaEndpoint($body);
|
||||
$request->session()->put('media-endpoint', $mediaEndpoint);
|
||||
if (array_key_exists('media-endpoint', $data)) {
|
||||
$user->mediaEndpoint = $data['media-endpoint'];
|
||||
}
|
||||
$user->save();
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method performs the actual POST request.
|
||||
* Update the syntax setting.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string The Micropub endpoint to post to
|
||||
* @param string The token to authenticate the request with
|
||||
* @return \GuzzleHttp\Response $response | \Illuminate\RedirectFactory redirect
|
||||
* @param Illuminate\Http\Request $request
|
||||
* @return Illuminate\Http\RedirectResponse
|
||||
* @todo validate input
|
||||
*/
|
||||
private function postNoteRequest(
|
||||
Request $request,
|
||||
$micropubEndpoint,
|
||||
$token
|
||||
) {
|
||||
$multipart = [
|
||||
[
|
||||
'name' => 'h',
|
||||
'contents' => 'entry',
|
||||
],
|
||||
[
|
||||
'name' => 'content',
|
||||
'contents' => $request->input('content'),
|
||||
],
|
||||
];
|
||||
if ($request->hasFile('photo')) {
|
||||
$photos = $request->file('photo');
|
||||
foreach ($photos as $photo) {
|
||||
$multipart[] = [
|
||||
'name' => 'photo[]',
|
||||
'contents' => fopen($photo->path(), 'r'),
|
||||
'filename' => $photo->getClientOriginalName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($request->input('in-reply-to') != '') {
|
||||
$multipart[] = [
|
||||
'name' => 'in-reply-to',
|
||||
'contents' => $request->input('in-reply-to'),
|
||||
];
|
||||
}
|
||||
if ($request->input('mp-syndicate-to')) {
|
||||
foreach ($request->input('mp-syndicate-to') as $syn) {
|
||||
$multipart[] = [
|
||||
'name' => 'mp-syndicate-to[]',
|
||||
'contents' => $syn,
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($request->input('location')) {
|
||||
if ($request->input('location') !== 'no-location') {
|
||||
$multipart[] = [
|
||||
'name' => 'location',
|
||||
'contents' => $request->input('location'),
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($request->input('media')) {
|
||||
foreach ($request->input('media') as $media) {
|
||||
$multipart[] = [
|
||||
'name' => 'photo[]',
|
||||
'contents' => $media,
|
||||
];
|
||||
}
|
||||
}
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
];
|
||||
try {
|
||||
$response = $this->guzzleClient->post($micropubEndpoint, [
|
||||
'multipart' => $multipart,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||
return redirect(route('micropub-client'))->with(
|
||||
'error',
|
||||
'There was a bad response from the micropub endpoint.'
|
||||
);
|
||||
}
|
||||
public function updateSyntax(Request $request)
|
||||
{
|
||||
$user = IndieWebUser::where('me', $request->session()->get('me'))->firstOrFail();
|
||||
$user->syntax = $request->syntax;
|
||||
$user->save();
|
||||
|
||||
return $response;
|
||||
return redirect(route('micropub-config'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -272,16 +394,17 @@ class MicropubClientController extends Controller
|
|||
*/
|
||||
public function newPlace(Request $request)
|
||||
{
|
||||
if ($request->session()->has('token') === false) {
|
||||
$url = normalize_url($request->session()->get('me'));
|
||||
$user = IndieWebUser::where('me', $url)->firstOrFail();
|
||||
|
||||
if ($user->token === null) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'error_description' => 'No known token',
|
||||
], 400);
|
||||
}
|
||||
$domain = $request->session()->get('me');
|
||||
$token = $request->session()->get('token');
|
||||
|
||||
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint($domain, $this->indieClient);
|
||||
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
|
||||
if (! $micropubEndpoint) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
|
|
@ -289,13 +412,27 @@ class MicropubClientController extends Controller
|
|||
], 400);
|
||||
}
|
||||
|
||||
$place = $this->postPlaceRequest($request, $micropubEndpoint, $token);
|
||||
if ($place === false) {
|
||||
$formParams = [
|
||||
'h' => 'card',
|
||||
'name' => $request->input('place-name'),
|
||||
'description' => $request->input('place-description'),
|
||||
'geo' => 'geo:' . $request->input('place-latitude') . ',' . $request->input('place-longitude'),
|
||||
];
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $user->token,
|
||||
];
|
||||
try {
|
||||
$response = $this->guzzleClient->request('POST', $micropubEndpoint, [
|
||||
'form_params' => $formParams,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
} catch (ClientException $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'error_description' => 'Unable to create the new place',
|
||||
], 400);
|
||||
}
|
||||
$place = $response->getHeader('Location')[0];
|
||||
|
||||
return response()->json([
|
||||
'uri' => $place,
|
||||
|
|
@ -305,44 +442,6 @@ class MicropubClientController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually make a micropub request to make a new place.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string The Micropub endpoint to post to
|
||||
* @param string The token to authenticate the request with
|
||||
* @param \GuzzleHttp\Client $client
|
||||
* @return \GuzzleHttp\Response $response | \Illuminate\RedirectFactory redirect
|
||||
*/
|
||||
private function postPlaceRequest(
|
||||
Request $request,
|
||||
$micropubEndpoint,
|
||||
$token
|
||||
) {
|
||||
$formParams = [
|
||||
'h' => 'card',
|
||||
'name' => $request->input('place-name'),
|
||||
'description' => $request->input('place-description'),
|
||||
'geo' => 'geo:' . $request->input('place-latitude') . ',' . $request->input('place-longitude'),
|
||||
];
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
];
|
||||
try {
|
||||
$response = $this->guzzleClient->request('POST', $micropubEndpoint, [
|
||||
'form_params' => $formParams,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
} catch (ClientException $e) {
|
||||
return false;
|
||||
}
|
||||
if ($response->getStatusCode() == 201) {
|
||||
return $response->getHeader('Location')[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the micropub endpoint requesting any nearby places.
|
||||
*
|
||||
|
|
@ -351,16 +450,17 @@ class MicropubClientController extends Controller
|
|||
*/
|
||||
public function nearbyPlaces(Request $request)
|
||||
{
|
||||
if ($request->session()->has('token') === false) {
|
||||
$url = normalize_url($request->session()->get('me'));
|
||||
$user = IndieWebUser::where('me', $url)->firstOrFail();
|
||||
|
||||
if ($user->token === null) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'error_description' => 'No known token',
|
||||
], 400);
|
||||
}
|
||||
$domain = $request->session()->get('me');
|
||||
$token = $request->session()->get('token');
|
||||
|
||||
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint($domain, $this->indieClient);
|
||||
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
|
||||
|
||||
if (! $micropubEndpoint) {
|
||||
return response()->json([
|
||||
|
|
@ -375,7 +475,7 @@ class MicropubClientController extends Controller
|
|||
$query .= ';u=' . $request->input('u');
|
||||
}
|
||||
$response = $this->guzzleClient->get($micropubEndpoint, [
|
||||
'headers' => ['Authorization' => 'Bearer ' . $token],
|
||||
'headers' => ['Authorization' => 'Bearer ' . $user->token],
|
||||
'query' => ['q' => $query],
|
||||
]);
|
||||
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
||||
|
|
@ -390,30 +490,34 @@ class MicropubClientController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Parse the syndication targets retreived from a cookie, to a form that can
|
||||
* be used in a view.
|
||||
* Parse the syndication targets JSON into a an array.
|
||||
*
|
||||
* @param string $syndicationTargets
|
||||
* @param string|null
|
||||
* @return array|null
|
||||
*/
|
||||
private function parseSyndicationTargets($syndicationTargets = null)
|
||||
{
|
||||
if ($syndicationTargets === null) {
|
||||
if ($syndicationTargets === null || $syndicationTargets === '') {
|
||||
return;
|
||||
}
|
||||
$syndicateTo = [];
|
||||
$data = json_decode($syndicationTargets, true);
|
||||
if (array_key_exists('syndicate-to', $data)) {
|
||||
foreach ($data['syndicate-to'] as $syn) {
|
||||
if (array_key_exists('uid', $data)) {
|
||||
$syndicateTo[] = [
|
||||
'target' => $data['uid'],
|
||||
'name' => $data['name'],
|
||||
];
|
||||
}
|
||||
foreach ($data as $syn) {
|
||||
if (array_key_exists('uid', $syn)) {
|
||||
$syndicateTo[] = [
|
||||
'target' => $syn['uid'],
|
||||
'name' => $syn['name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
if (count($syndicateTo) > 0) {
|
||||
return $syndicateTo;
|
||||
}
|
||||
|
||||
return $syndicateTo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use App\{Media, Place};
|
||||
use App\{Media, Note, Place};
|
||||
use Illuminate\Http\{Request, Response};
|
||||
use App\Exceptions\InvalidTokenException;
|
||||
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
|
||||
use App\Services\{NoteService, PlaceService, TokenService};
|
||||
|
||||
|
|
@ -47,34 +48,67 @@ class MicropubController extends Controller
|
|||
*/
|
||||
public function post(Request $request)
|
||||
{
|
||||
$httpAuth = $request->header('Authorization');
|
||||
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) {
|
||||
$token = $match[1];
|
||||
$tokenData = $this->tokenService->validateToken($token);
|
||||
if ($tokenData->hasClaim('scope')) {
|
||||
$scopes = explode(' ', $tokenData->getClaim('scope'));
|
||||
if (array_search('post', $scopes) !== false) {
|
||||
$clientId = $tokenData->getClaim('client_id');
|
||||
if (($request->input('h') == 'entry') || ($request->input('type')[0] == 'h-entry')) {
|
||||
$data = [];
|
||||
$data['client-id'] = $clientId;
|
||||
if ($request->header('Content-Type') == 'application/json') {
|
||||
$data['content'] = $request->input('properties.content')[0];
|
||||
$data['in-reply-to'] = $request->input('properties.in-reply-to')[0];
|
||||
$data['location'] = $request->input('properties.location');
|
||||
//flatten location if array
|
||||
if (is_array($data['location'])) {
|
||||
$data['location'] = $data['location'][0];
|
||||
}
|
||||
} else {
|
||||
$data['content'] = $request->input('content');
|
||||
$data['in-reply-to'] = $request->input('in-reply-to');
|
||||
$data['location'] = $request->input('location');
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->bearerToken());
|
||||
} catch (InvalidTokenException $e) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The provided token did not pass validation',
|
||||
], 400);
|
||||
}
|
||||
if ($tokenData->hasClaim('scope')) {
|
||||
if (($request->input('h') == 'entry') || ($request->input('type')[0] == 'h-entry')) {
|
||||
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
|
||||
return $this->returnInsufficientScopeResponse();
|
||||
}
|
||||
$data = [];
|
||||
$data['client-id'] = $tokenData->getClaim('client_id');
|
||||
if ($request->header('Content-Type') == 'application/json') {
|
||||
if (is_string($request->input('properties.content')[0])) {
|
||||
$data['content'] = $request->input('properties.content')[0]; //plaintext content
|
||||
}
|
||||
if (is_array($request->input('properties.content')[0])
|
||||
&& array_key_exists('html', $request->input('properties.content')[0])
|
||||
) {
|
||||
$data['content'] = $request->input('properties.content')[0]['html'];
|
||||
}
|
||||
$data['in-reply-to'] = $request->input('properties.in-reply-to')[0];
|
||||
$data['location'] = $request->input('properties.location');
|
||||
//flatten location if array
|
||||
if (is_array($data['location'])) {
|
||||
$data['location'] = $data['location'][0];
|
||||
}
|
||||
$data['published'] = $request->input('properties.published')[0];
|
||||
//create checkin place
|
||||
if (array_key_exists('checkin', $request->input('properties'))) {
|
||||
$data['checkin'] = $request->input('properties.checkin.0.properties.url.0');
|
||||
try {
|
||||
$this->placeService->createPlaceFromCheckin($request->input('properties.checkin.0'));
|
||||
} catch (\Exception $e) {
|
||||
$data['checkin'] = null;
|
||||
}
|
||||
$data['syndicate'] = [];
|
||||
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
|
||||
if (is_string($request->input('mp-syndicate-to'))) {
|
||||
$service = array_search($request->input('mp-syndicate-to'));
|
||||
}
|
||||
} else {
|
||||
$data['content'] = $request->input('content');
|
||||
$data['in-reply-to'] = $request->input('in-reply-to');
|
||||
$data['location'] = $request->input('location');
|
||||
$data['published'] = $request->input('published');
|
||||
}
|
||||
$data['syndicate'] = [];
|
||||
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
|
||||
if (is_string($request->input('mp-syndicate-to'))) {
|
||||
$service = array_search($request->input('mp-syndicate-to'));
|
||||
if ($service == 'Twitter') {
|
||||
$data['syndicate'][] = 'twitter';
|
||||
}
|
||||
if ($service == 'Facebook') {
|
||||
$data['syndicate'][] = 'facebook';
|
||||
}
|
||||
}
|
||||
if (is_array($request->input('mp-syndicate-to'))) {
|
||||
foreach ($targets as $service => $target) {
|
||||
if (in_array($target, $request->input('mp-syndicate-to'))) {
|
||||
if ($service == 'Twitter') {
|
||||
$data['syndicate'][] = 'twitter';
|
||||
}
|
||||
|
|
@ -82,83 +116,145 @@ class MicropubController extends Controller
|
|||
$data['syndicate'][] = 'facebook';
|
||||
}
|
||||
}
|
||||
if (is_array($request->input('mp-syndicate-to'))) {
|
||||
foreach ($targets as $service => $target) {
|
||||
if (in_array($target, $request->input('mp-syndicate-to'))) {
|
||||
if ($service == 'Twitter') {
|
||||
$data['syndicate'][] = 'twitter';
|
||||
}
|
||||
if ($service == 'Facebook') {
|
||||
$data['syndicate'][] = 'facebook';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$data['photo'] = [];
|
||||
if (is_array($request->input('photo'))) {
|
||||
foreach ($request->input('photo') as $photo) {
|
||||
if (is_string($photo)) {
|
||||
//only supporting media URLs for now
|
||||
$data['photo'][] = $photo;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
$note = $this->noteService->createNote($data);
|
||||
} catch (Exception $exception) {
|
||||
return response()->json(['error' => true], 400);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => $note->longurl,
|
||||
], 201)->header('Location', $note->longurl);
|
||||
}
|
||||
if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') {
|
||||
$data = [];
|
||||
if ($request->header('Content-Type') == 'application/json') {
|
||||
$data['name'] = $request->input('properties.name');
|
||||
$data['description'] = $request->input('properties.description') ?? null;
|
||||
if ($request->has('properties.geo')) {
|
||||
$data['geo'] = $request->input('properties.geo');
|
||||
}
|
||||
$data['photo'] = [];
|
||||
if (is_array($request->input('photo'))) {
|
||||
foreach ($request->input('photo') as $photo) {
|
||||
if (is_string($photo)) {
|
||||
//only supporting media URLs for now
|
||||
$data['photo'][] = $photo;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
$note = $this->noteService->createNote($data);
|
||||
} catch (\Exception $exception) {
|
||||
return response()->json(['error' => true], 400);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => $note->longurl,
|
||||
], 201)->header('Location', $note->longurl);
|
||||
}
|
||||
if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') {
|
||||
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
|
||||
return $this->returnInsufficientScopeResponse();
|
||||
}
|
||||
$data = [];
|
||||
if ($request->header('Content-Type') == 'application/json') {
|
||||
$data['name'] = $request->input('properties.name');
|
||||
$data['description'] = $request->input('properties.description') ?? null;
|
||||
if ($request->has('properties.geo')) {
|
||||
$data['geo'] = $request->input('properties.geo');
|
||||
}
|
||||
} else {
|
||||
$data['name'] = $request->input('name');
|
||||
$data['description'] = $request->input('description');
|
||||
if ($request->has('geo')) {
|
||||
$data['geo'] = $request->input('geo');
|
||||
}
|
||||
if ($request->has('latitude')) {
|
||||
$data['latitude'] = $request->input('latitude');
|
||||
$data['longitude'] = $request->input('longitude');
|
||||
}
|
||||
}
|
||||
try {
|
||||
$place = $this->placeService->createPlace($data);
|
||||
} catch (\Exception $exception) {
|
||||
return response()->json(['error' => true], 400);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => $place->longurl,
|
||||
], 201)->header('Location', $place->longurl);
|
||||
}
|
||||
if ($request->input('action') == 'update') {
|
||||
if (stristr($tokenData->getClaim('scope'), 'update') === false) {
|
||||
return $this->returnInsufficientScopeResponse();
|
||||
}
|
||||
$urlPath = parse_url($request->input('url'), PHP_URL_PATH);
|
||||
//is it a note we are updating?
|
||||
if (mb_substr($urlPath, 1, 5) === 'notes') {
|
||||
try {
|
||||
$note = Note::nb60(basename($urlPath))->first();
|
||||
} catch (\Exception $exception) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'No known note with given ID',
|
||||
]);
|
||||
}
|
||||
//got the note, are we dealing with a “replace” request?
|
||||
if ($request->has('replace')) {
|
||||
foreach ($request->input('replace') as $property => $value) {
|
||||
if ($property == 'content') {
|
||||
$note->note = $value[0];
|
||||
}
|
||||
} else {
|
||||
$data['name'] = $request->input('name');
|
||||
$data['description'] = $request->input('description');
|
||||
if ($request->has('geo')) {
|
||||
$data['geo'] = $request->input('geo');
|
||||
}
|
||||
if ($request->has('latitude')) {
|
||||
$data['latitude'] = $request->input('latitude');
|
||||
$data['longitude'] = $request->input('longitude');
|
||||
if ($property == 'syndication') {
|
||||
foreach ($value as $syndicationURL) {
|
||||
if (starts_with($syndicationURL, 'https://www.facebook.com')) {
|
||||
$note->facebook_url = $syndicationURL;
|
||||
}
|
||||
if (starts_with($syndicationURL, 'https://www.swarmapp.com')) {
|
||||
$note->swarm_url = $syndicationURL;
|
||||
}
|
||||
if (starts_with($syndicationURL, 'https://twitter.com')) {
|
||||
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
$place = $this->placeService->createPlace($data);
|
||||
} catch (Exception $exception) {
|
||||
return response()->json(['error' => true], 400);
|
||||
}
|
||||
$note->save();
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => $place->longurl,
|
||||
], 201)->header('Location', $place->longurl);
|
||||
'response' => 'updated',
|
||||
]);
|
||||
}
|
||||
//how about “add”
|
||||
if ($request->has('add')) {
|
||||
foreach ($request->input('add') as $property => $value) {
|
||||
if ($property == 'syndication') {
|
||||
foreach ($value as $syndicationURL) {
|
||||
if (starts_with($syndicationURL, 'https://www.facebook.com')) {
|
||||
$note->facebook_url = $syndicationURL;
|
||||
}
|
||||
if (starts_with($syndicationURL, 'https://www.swarmapp.com')) {
|
||||
$note->swarm_url = $syndicationURL;
|
||||
}
|
||||
if (starts_with($syndicationURL, 'https://twitter.com')) {
|
||||
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($property == 'photo') {
|
||||
foreach ($value as $photoURL) {
|
||||
if (start_with($photo, 'https://')) {
|
||||
$media = new Media();
|
||||
$media->path = $photoURL;
|
||||
$media->type = 'image';
|
||||
$media->save();
|
||||
$note->media()->save($media);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$note->save();
|
||||
|
||||
return response()->json([
|
||||
'response' => 'updated',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The token provided is not valid or does not have the necessary scope',
|
||||
], 400);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'no_token',
|
||||
'error_description' => 'No OAuth token sent with request',
|
||||
], 400);
|
||||
'error' => 'forbidden',
|
||||
'error_description' => 'The token has no scopes',
|
||||
], 403);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,68 +268,58 @@ class MicropubController extends Controller
|
|||
*/
|
||||
public function get(Request $request)
|
||||
{
|
||||
$httpAuth = $request->header('Authorization');
|
||||
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) {
|
||||
$token = $match[1];
|
||||
$valid = $this->tokenService->validateToken($token);
|
||||
|
||||
if ($valid === null) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The provided token did not pass validation',
|
||||
], 400);
|
||||
}
|
||||
//we have a valid token, is `syndicate-to` set?
|
||||
if ($request->input('q') === 'syndicate-to') {
|
||||
return response()->json([
|
||||
'syndicate-to' => config('syndication.targets'),
|
||||
]);
|
||||
}
|
||||
|
||||
//nope, how about a config query?
|
||||
if ($request->input('q') == 'config') {
|
||||
return response()->json([
|
||||
'syndicate-to' => config('syndication.targets'),
|
||||
'media-endpoint' => route('media-endpoint'),
|
||||
]);
|
||||
}
|
||||
|
||||
//nope, how about a geo URL?
|
||||
if (substr($request->input('q'), 0, 4) === 'geo:') {
|
||||
preg_match_all(
|
||||
'/([0-9\.\-]+)/',
|
||||
$request->input('q'),
|
||||
$matches
|
||||
);
|
||||
$distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000;
|
||||
$places = Place::near($matches[0][0], $matches[0][1], $distance);
|
||||
foreach ($places as $place) {
|
||||
$place->uri = config('app.url') . '/places/' . $place->slug;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'places',
|
||||
'places' => $places,
|
||||
]);
|
||||
}
|
||||
|
||||
//nope, just return the token
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->bearerToken());
|
||||
} catch (InvalidTokenException $e) {
|
||||
return response()->json([
|
||||
'response' => 'token',
|
||||
'token' => [
|
||||
'me' => $valid->getClaim('me'),
|
||||
'scope' => $valid->getClaim('scope'),
|
||||
'client_id' => $valid->getClaim('client_id'),
|
||||
],
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The provided token did not pass validation',
|
||||
], 400);
|
||||
}
|
||||
//we have a valid token, is `syndicate-to` set?
|
||||
if ($request->input('q') === 'syndicate-to') {
|
||||
return response()->json([
|
||||
'syndicate-to' => config('syndication.targets'),
|
||||
]);
|
||||
}
|
||||
|
||||
//nope, how about a config query?
|
||||
if ($request->input('q') == 'config') {
|
||||
return response()->json([
|
||||
'syndicate-to' => config('syndication.targets'),
|
||||
'media-endpoint' => route('media-endpoint'),
|
||||
]);
|
||||
}
|
||||
|
||||
//nope, how about a geo URL?
|
||||
if (substr($request->input('q'), 0, 4) === 'geo:') {
|
||||
preg_match_all(
|
||||
'/([0-9\.\-]+)/',
|
||||
$request->input('q'),
|
||||
$matches
|
||||
);
|
||||
$distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000;
|
||||
$places = Place::near($matches[0][0], $matches[0][1], $distance);
|
||||
foreach ($places as $place) {
|
||||
$place->uri = config('app.url') . '/places/' . $place->slug;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'places',
|
||||
'places' => $places,
|
||||
]);
|
||||
}
|
||||
|
||||
//nope, just return the token
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'no_token',
|
||||
'error_description' => 'No token provided with request',
|
||||
], 400);
|
||||
'response' => 'token',
|
||||
'token' => [
|
||||
'me' => $tokenData->getClaim('me'),
|
||||
'scope' => $tokenData->getClaim('scope'),
|
||||
'client_id' => $tokenData->getClaim('client_id'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -244,82 +330,65 @@ class MicropubController extends Controller
|
|||
*/
|
||||
public function media(Request $request)
|
||||
{
|
||||
//can this go in middleware
|
||||
$httpAuth = $request->header('Authorization');
|
||||
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) {
|
||||
$token = $match[1];
|
||||
$tokenData = $this->tokenService->validateToken($token);
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->bearerToken());
|
||||
} catch (InvalidTokenException $e) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The provided token did not pass validation',
|
||||
], 400);
|
||||
}
|
||||
|
||||
if ($tokenData === null) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The provided token did not pass validation',
|
||||
], 400);
|
||||
//check post scope
|
||||
if ($tokenData->hasClaim('scope')) {
|
||||
if (stristr($token->getClaim('scope'), 'post') === false) {
|
||||
return $this->returnInsufficientScopeResponse();
|
||||
}
|
||||
|
||||
//check post scope
|
||||
if ($tokenData->hasClaim('scope')) {
|
||||
$scopes = explode(' ', $tokenData->getClaim('scope'));
|
||||
if (array_search('post', $scopes) !== false) {
|
||||
//check media valid
|
||||
if ($request->hasFile('file') && $request->file('file')->isValid()) {
|
||||
$type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType());
|
||||
try {
|
||||
$filename = Uuid::uuid4() . '.' . $request->file('file')->extension();
|
||||
} catch (UnsatisfiedDependencyException $e) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'internal_server_error',
|
||||
'error_description' => 'A problem occured handling your request',
|
||||
], 500);
|
||||
}
|
||||
try {
|
||||
$path = $request->file('file')->storeAs('media', $filename, 's3');
|
||||
} catch (Exception $e) { // which exception?
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'service_unavailable',
|
||||
'error_description' => 'Unable to save media to S3',
|
||||
], 503);
|
||||
}
|
||||
$media = new Media();
|
||||
$media->token = $token;
|
||||
$media->path = $path;
|
||||
$media->type = $type;
|
||||
$media->save();
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => $media->url,
|
||||
], 201)->header('Location', $media->url);
|
||||
}
|
||||
|
||||
//check media valid
|
||||
if ($request->hasFile('file') && $request->file('file')->isValid()) {
|
||||
$type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType());
|
||||
try {
|
||||
$filename = Uuid::uuid4() . '.' . $request->file('file')->extension();
|
||||
} catch (UnsatisfiedDependencyException $e) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'The uploaded file failed validation',
|
||||
], 400);
|
||||
'error' => 'internal_server_error',
|
||||
'error_description' => 'A problem occured handling your request',
|
||||
], 500);
|
||||
}
|
||||
try {
|
||||
$path = $request->file('file')->storeAs('media', $filename, 's3');
|
||||
} catch (Exception $e) { // which exception?
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'service_unavailable',
|
||||
'error_description' => 'Unable to save media to S3',
|
||||
], 503);
|
||||
}
|
||||
$media = new Media();
|
||||
$media->token = $request->bearerToken();
|
||||
$media->path = $path;
|
||||
$media->type = $type;
|
||||
$media->save();
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'insufficient_scope',
|
||||
'error_description' => 'The provided token has insufficient scopes',
|
||||
], 401);
|
||||
'response' => 'created',
|
||||
'location' => $media->url,
|
||||
], 201)->header('Location', $media->url);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'unauthorized',
|
||||
'error_description' => 'No token provided with request',
|
||||
], 401);
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'The uploaded file failed validation',
|
||||
], 400);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'no_token',
|
||||
'error_description' => 'There was no token provided with the request',
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'The provided token has no scopes',
|
||||
], 400);
|
||||
}
|
||||
|
||||
|
|
@ -366,4 +435,13 @@ class MicropubController extends Controller
|
|||
|
||||
return 'download';
|
||||
}
|
||||
|
||||
private function returnInsufficientScopeResponse()
|
||||
{
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'insufficient_scope',
|
||||
'error_description' => 'The token’s scope does not have the necessary requirements.',
|
||||
], 401);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
79
app/Http/Controllers/TokenEndpointController.php
Normal file
79
app/Http/Controllers/TokenEndpointController.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use IndieAuth\Client;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\TokenService;
|
||||
|
||||
class TokenEndpointController extends Controller
|
||||
{
|
||||
/**
|
||||
* The IndieAuth Client.
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* The Token handling service.
|
||||
*/
|
||||
protected $tokenService;
|
||||
|
||||
/**
|
||||
* Inject the dependencies.
|
||||
*
|
||||
* @param \IndieAuth\Client $client
|
||||
* @param \App\Services\TokenService $tokenService
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
Client $client = null,
|
||||
TokenService $tokenService = null
|
||||
) {
|
||||
$this->client = $client ?? new Client();
|
||||
$this->tokenService = $tokenService ?? new TokenService();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user has auth’d via the IndieAuth protocol, issue a valid token.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$authorizationEndpoint = $this->client->discoverAuthorizationEndpoint(normalize_url($request->input('me')));
|
||||
if ($authorizationEndpoint) {
|
||||
$auth = $this->client->verifyIndieAuthCode(
|
||||
$authorizationEndpoint,
|
||||
$request->input('code'),
|
||||
$request->input('me'),
|
||||
$request->input('redirect_uri'),
|
||||
$request->input('client_id'),
|
||||
$request->input('state')
|
||||
);
|
||||
if (array_key_exists('me', $auth)) {
|
||||
$scope = $auth['scope'] ?? '';
|
||||
$tokenData = [
|
||||
'me' => $request->input('me'),
|
||||
'client_id' => $request->input('client_id'),
|
||||
'scope' => $scope,
|
||||
];
|
||||
$token = $this->tokenService->getNewToken($tokenData);
|
||||
$content = http_build_query([
|
||||
'me' => $request->input('me'),
|
||||
'scope' => $scope,
|
||||
'access_token' => $token,
|
||||
]);
|
||||
|
||||
return response($content)->header(
|
||||
'Content-Type',
|
||||
'application/x-www-form-urlencoded'
|
||||
);
|
||||
}
|
||||
|
||||
return response('There was an error verifying the authorisation code.', 400);
|
||||
}
|
||||
|
||||
return response('Can’t determine the authorisation endpoint.', 400);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,8 @@ class Kernel extends HttpKernel
|
|||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\LinkHeadersMiddleware::class,
|
||||
\App\Http\Middleware\DevTokenMiddleware::class,
|
||||
//\App\Http\Middleware\DevTokenMiddleware::class,
|
||||
\App\Http\Middleware\LocalhostSessionMiddleware::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
|
|
@ -58,5 +59,6 @@ class Kernel extends HttpKernel
|
|||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'myauth' => \App\Http\Middleware\MyAuthMiddleware::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'micropub.token' => \App\Http\Middleware\VerifyMicropubToken::class,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
26
app/Http/Middleware/LocalhostSessionMiddleware.php
Normal file
26
app/Http/Middleware/LocalhostSessionMiddleware.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class LocalhostSessionMiddleware
|
||||
{
|
||||
/**
|
||||
* Whilst we are developing locally, automatically log in as
|
||||
* `['me' => config('app.url')]` as I can’t manually log in as
|
||||
* a .localhost domain.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (config('app.env') !== 'production') {
|
||||
session(['me' => config('app.url')]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
28
app/Http/Middleware/VerifyMicropubToken.php
Normal file
28
app/Http/Middleware/VerifyMicropubToken.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class VerifyMicropubToken
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($request->bearerToken() === null) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'unauthorized',
|
||||
'error_description' => 'No access token was provided in the request',
|
||||
], 401);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
15
app/IndieWebUser.php
Normal file
15
app/IndieWebUser.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class IndieWebUser extends Model
|
||||
{
|
||||
/**
|
||||
* Mass assignment protection.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['me'];
|
||||
}
|
||||
|
|
@ -28,6 +28,10 @@ class Media extends Model
|
|||
*/
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
if (starts_with($this->path, 'https://')) {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
return config('filesystems.disks.s3.url') . '/' . $this->path;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
app/Note.php
14
app/Note.php
|
|
@ -191,6 +191,20 @@ class Note extends Model
|
|||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to select a note via a NewBase60 id.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $nb60id
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeNb60($query, $nb60id)
|
||||
{
|
||||
$numbers = new Numbers();
|
||||
|
||||
return $query->where('id', $numbers->b60tonum($nb60id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Take note that this method does two things, given @username (NOT [@username](URL)!)
|
||||
* we try to create a fancy hcard from our contact info. If this is not possible
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
//Add tags for notes
|
||||
Note::created(function ($note) {
|
||||
$tagsToAdd = [];
|
||||
preg_match_all('/#([^\s<>]+)\b/', $note, $tags);
|
||||
preg_match_all('/#([^\s<>]+)\b/', $note->note, $tags);
|
||||
foreach ($tags[1] as $tag) {
|
||||
$tag = Tag::normalizeTag($tag);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class EventServiceProvider extends ServiceProvider
|
|||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
'App\Events\SomeEvent' => [
|
||||
'App\Events\Event' => [
|
||||
'App\Listeners\EventListener',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -45,14 +45,12 @@ class IndieAuthService
|
|||
session(['state' => $state]);
|
||||
$redirectURL = route('indieauth-callback');
|
||||
$clientId = route('micropub-client');
|
||||
$scope = 'post';
|
||||
$authorizationURL = $this->client->buildAuthorizationURL(
|
||||
$authEndpoint,
|
||||
$this->client->normalizeMeURL($domain),
|
||||
$redirectURL,
|
||||
$clientId,
|
||||
$state,
|
||||
$scope
|
||||
$state
|
||||
);
|
||||
|
||||
return $authorizationURL;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@ class NoteService
|
|||
*/
|
||||
public function createNote(array $data): Note
|
||||
{
|
||||
//check the input
|
||||
if (array_key_exists('content', $data) === false) {
|
||||
throw new \Exception('No content defined'); //we can’t fudge the data
|
||||
}
|
||||
if (array_key_exists('in-reply-to', $data) === false) {
|
||||
$data['in-reply-to'] = null;
|
||||
}
|
||||
if (array_key_exists('client-id', $data) === false) {
|
||||
$data['client-id'] = null;
|
||||
}
|
||||
$note = Note::create(
|
||||
[
|
||||
'note' => $data['content'],
|
||||
|
|
@ -25,6 +35,11 @@ class NoteService
|
|||
]
|
||||
);
|
||||
|
||||
if (array_key_exists('published', $data) && empty($data['published']) === false) {
|
||||
$carbon = new \Carbon\Carbon($data['published']);
|
||||
$note->created_at = $note->updated_at = $carbon->toDateTimeString();
|
||||
}
|
||||
|
||||
if (array_key_exists('location', $data) && $data['location'] !== null && $data['location'] !== 'no-location') {
|
||||
if (starts_with($data['location'], config('app.url'))) {
|
||||
//uri of form http://host/places/slug, we want slug
|
||||
|
|
@ -44,6 +59,13 @@ class NoteService
|
|||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('checkin', $data) && $data['checkin'] !== null) {
|
||||
$place = Place::where('foursquare', $data['checkin'])->first();
|
||||
if ($place !== null) {
|
||||
$note->place()->associate($place);
|
||||
}
|
||||
}
|
||||
|
||||
/* drop image support for now
|
||||
//add images to media library
|
||||
if ($request->hasFile('photo')) {
|
||||
|
|
@ -55,12 +77,17 @@ class NoteService
|
|||
*/
|
||||
//add support for media uploaded as URLs
|
||||
foreach ($data['photo'] as $photo) {
|
||||
// check the media was uploaded to my endpoint
|
||||
// check the media was uploaded to my endpoint, and use path
|
||||
if (starts_with($photo, config('filesystems.disks.s3.url'))) {
|
||||
$path = substr($photo, strlen(config('filesystems.disks.s3.url')));
|
||||
$media = Media::where('path', ltrim($path, '/'))->firstOrFail();
|
||||
$note->media()->save($media);
|
||||
} else {
|
||||
$media = Media::firstOrNew(['path' => $photo]);
|
||||
// currently assuming this is a photo from Swarm
|
||||
$media->type = 'image';
|
||||
$media->save();
|
||||
}
|
||||
$note->media()->save($media);
|
||||
}
|
||||
|
||||
$note->save();
|
||||
|
|
|
|||
|
|
@ -36,4 +36,39 @@ class PlaceService
|
|||
|
||||
return $place;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a place from a h-card checkin, for exameple from OwnYourSwarm.
|
||||
*
|
||||
* @param array
|
||||
* @return bool
|
||||
*/
|
||||
public function createPlaceFromCheckin(array $checkin): bool
|
||||
{
|
||||
//check if the place exists if from swarm
|
||||
if (array_key_exists('url', $checkin['properties'])) {
|
||||
$search = Place::where('foursquare', $checkin['properties']['url'][0])->count();
|
||||
if ($search === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (array_key_exists('name', $checkin['properties']) === false) {
|
||||
throw new \InvalidArgumentException('Missing required name');
|
||||
}
|
||||
if (array_key_exists('latitude', $checkin['properties']) === false) {
|
||||
throw new \InvalidArgumentException('Missing required longitude/latitude');
|
||||
}
|
||||
$place = new Place();
|
||||
$place->name = $checkin['properties']['name'][0];
|
||||
if (starts_with($checkin['properties']['url'][0], 'https://foursquare.com')) {
|
||||
$place->foursquare = $checkin['properties']['url'][0];
|
||||
}
|
||||
$place->location = new Point(
|
||||
(float) $checkin['properties']['latitude'][0],
|
||||
(float) $checkin['properties']['longitude'][0]
|
||||
);
|
||||
$place->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue