Merge branch 'release/0.5'

This commit is contained in:
Jonny Barnes 2017-05-18 15:32:49 +01:00
commit a7117b3972
55 changed files with 2324 additions and 1231 deletions

View file

@ -22,7 +22,7 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_DRIVER=smtp MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
@ -52,3 +52,5 @@ TWITTER_ACCESS_TOKEN_SECRET=
SCOUT_DRIVER=pgsql SCOUT_DRIVER=pgsql
PIWIK=false PIWIK=false
PSYSH_CONFIG=tinker.config.php

1
.gitattributes vendored
View file

@ -1,3 +1,4 @@
* text=auto * text=auto
*.css linguist-vendored *.css linguist-vendored
*.scss linguist-vendored *.scss linguist-vendored
*.js linguist-vendored

View file

@ -19,6 +19,7 @@ addons:
paths: paths:
- $(ls tests/Browser/screenshots/*.png | tr "\n" ":") - $(ls tests/Browser/screenshots/*.png | tr "\n" ":")
- $(ls tests/Browser/console/*.log | tr "\n" ":") - $(ls tests/Browser/console/*.log | tr "\n" ":")
- $(ls storage/logs/*.log | tr "\n" ":")
- $(ls /tmp/*.log | tr "\n" ":") - $(ls /tmp/*.log | tr "\n" ":")
services: services:
@ -49,16 +50,18 @@ install:
- travis/install-nginx.sh - travis/install-nginx.sh
before_script: 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 -c 'create database travis_ci_test'
- psql -U travis -d travis_ci_test -c 'create extension postgis' - psql -U travis -d travis_ci_test -c 'create extension postgis'
- cp .env.travis .env - cp .env.travis .env
- php artisan key:generate - php artisan key:generate
- php artisan migrate - php artisan migrate
- php artisan db:seed - php artisan db:seed
- php artisan token:generate
- phantomjs --webdriver=127.0.0.1:9515 --webdriver-loglevel=DEBUG & - phantomjs --webdriver=127.0.0.1:9515 --webdriver-loglevel=DEBUG &
- sleep 5 # Give artisan some time to start serving - sleep 5 # Give artisan some time to start serving
script: script:
- php vendor/bin/phpunit --coverage-text - php vendor/bin/phpunit --coverage-text
- php artisan dusk - php artisan dusk
- php artisan security:check - php vendor/bin/security-checker security:check ./composer.lock --end-point=http://security.sensiolabs.org/check_lock

View file

@ -2,9 +2,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\IndieWebUser;
use App\Services\TokenService; use App\Services\TokenService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class GenerateToken extends Command class GenerateToken extends Command
{ {
@ -49,10 +49,12 @@ class GenerateToken extends Command
$data = [ $data = [
'me' => config('app.url'), 'me' => config('app.url'),
'client_id' => route('micropub-client'), 'client_id' => route('micropub-client'),
'scope' => 'post', 'scope' => 'create update',
]; ];
$token = $tokenService->getNewToken($data); $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'); $this->info('Set token');
} }

View 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);
}
}

View file

@ -2,35 +2,26 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\IndieWebUser;
use IndieAuth\Client;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Services\TokenService;
use App\Services\IndieAuthService;
class IndieAuthController extends Controller class IndieAuthController extends Controller
{ {
/** /**
* This service isolates the IndieAuth Client code. * The IndieAuth Client.
*/ */
protected $indieAuthService; protected $client;
/** /**
* The Token handling service. * Inject the dependency.
*/
protected $tokenService;
/**
* Inject the dependencies.
* *
* @param \App\Services\IndieAuthService $indieAuthService * @param \IndieAuth\Client $client
* @param \App\Services\TokenService $tokenService
* @return void * @return void
*/ */
public function __construct( public function __construct(Client $client = null)
IndieAuthService $indieAuthService = null, {
TokenService $tokenService = null $this->client = $client ?? new Client();
) {
$this->indieAuthService = $indieAuthService ?? new IndieAuthService();
$this->tokenService = $tokenService ?? new TokenService();
} }
/** /**
@ -44,25 +35,31 @@ class IndieAuthController extends Controller
*/ */
public function start(Request $request) public function start(Request $request)
{ {
$authorizationEndpoint = $this->indieAuthService->getAuthorizationEndpoint( $url = normalize_url($request->input('me'));
$request->input('me') $authorizationEndpoint = $this->client->discoverAuthorizationEndpoint($url);
); if ($authorizationEndpoint != null) {
if ($authorizationEndpoint !== null) { $state = bin2hex(openssl_random_pseudo_bytes(16));
$authorizationURL = $this->indieAuthService->buildAuthorizationURL( session(['state' => $state]);
$authorizationURL = $this->client->buildAuthorizationURL(
$authorizationEndpoint, $authorizationEndpoint,
$request->input('me') $url,
route('indieauth-callback'), //redirect_uri
route('micropub-client'), //client_id
$state
); );
if ($authorizationURL) { if ($authorizationURL) {
return redirect($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'); return redirect(route('micropub-client'))->with('error', 'Unable to determine authorisation endpoint');
} }
/** /**
* Once they have verified themselves through the authorisation endpint * Once they have verified themselves through the authorisation endpoint
* the next step is retreiveing a token from the token endpoint. * the next step is register/login the user.
* *
* @param \Illuminate\Http\Rrequest $request * @param \Illuminate\Http\Rrequest $request
* @return \Illuminate\Routing\RedirectResponse redirect * @return \Illuminate\Routing\RedirectResponse redirect
@ -75,38 +72,16 @@ class IndieAuthController extends Controller
'Invalid <code>state</code> value returned from indieauth server' '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)) { $url = normalize_url($request->input('me'));
$request->session()->put('me', $token['me']); $indiewebUser = IndieWebUser::firstOrCreate(['me' => $url]);
$request->session()->put('token', $token['access_token']); $request->session()->put(['me' => $url]);
return redirect(route('micropub-client')); return redirect(route('micropub-client'));
} }
return redirect(route('micropub-client'))->with(
'error',
'Unable to get a token from the endpoint'
);
}
/** /**
* 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 * @return \Illuminate\Routing\RedirectResponse redirect
*/ */
@ -114,44 +89,6 @@ class IndieAuthController extends Controller
{ {
$request->session()->flush(); $request->session()->flush();
return redirect(route('micropub-client'))->cookie('me', 'loggedout', 1); return redirect(route('micropub-client'));
}
/**
* If the user has authd 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);
} }
} }

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Services\IndieAuthService; use App\IndieWebUser;
use IndieAuth\Client as IndieClient; use IndieAuth\Client as IndieClient;
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Http\{Request, Response}; use Illuminate\Http\{Request, Response};
@ -10,20 +10,13 @@ use GuzzleHttp\Exception\{ClientException, ServerException};
class MicropubClientController extends Controller class MicropubClientController extends Controller
{ {
/**
* The IndieAuth service container.
*/
protected $indieAuthService;
/** /**
* Inject the dependencies. * Inject the dependencies.
*/ */
public function __construct( public function __construct(
IndieAuthService $indieAuthService = null,
IndieClient $indieClient = null, IndieClient $indieClient = null,
GuzzleClient $guzzleClient = null GuzzleClient $guzzleClient = null
) { ) {
$this->indieAuthService = $indieAuthService ?? new IndieAuthService();
$this->guzzleClient = $guzzleClient ?? new GuzzleClient(); $this->guzzleClient = $guzzleClient ?? new GuzzleClient();
$this->indieClient = $indieClient ?? new IndieClient(); $this->indieClient = $indieClient ?? new IndieClient();
} }
@ -37,8 +30,11 @@ class MicropubClientController extends Controller
public function create(Request $request) public function create(Request $request)
{ {
$url = $request->session()->get('me'); $url = $request->session()->get('me');
$syndication = $request->session()->get('syndication'); if ($url) {
$mediaEndpoint = $request->session()->get('media-endpoint'); $indiewebUser = IndieWebUser::where('me', $url)->first();
}
$syndication = $this->parseSyndicationTargets($indiewebUser->syndication);
$mediaEndpoint = $indiewebUser->mediaEndpoint ?? null;
$mediaURLs = $request->session()->get('media-links'); $mediaURLs = $request->session()->get('media-links');
return view('micropub.create', compact('url', 'syndication', 'mediaEndpoint', 'mediaURLs')); return view('micropub.create', compact('url', 'syndication', 'mediaEndpoint', 'mediaURLs'));
@ -56,19 +52,17 @@ class MicropubClientController extends Controller
return back(); return back();
} }
$mediaEndpoint = $request->session()->get('media-endpoint'); $user = IndieWebUser::where('me', $request->session()->get('me'))->firstOrFail();
if ($mediaEndpoint == null) { if ($user->mediaEndpoint == null || $user->token == null) {
return back(); return back();
} }
$token = $request->session()->get('token');
$mediaURLs = []; $mediaURLs = [];
foreach ($request->file('file') as $file) { foreach ($request->file('file') as $file) {
try { try {
$response = $this->guzzleClient->request('POST', $mediaEndpoint, [ $response = $this->guzzleClient->request('POST', $user->mediaEndpoint, [
'headers' => [ 'headers' => [
'Authorization' => 'Bearer ' . $token, 'Authorization' => 'Bearer ' . $user->token,
], ],
'multipart' => [ 'multipart' => [
[ [
@ -109,93 +103,23 @@ class MicropubClientController extends Controller
*/ */
public function store(Request $request) public function store(Request $request)
{ {
$domain = $request->session()->get('me'); $url = normalize_url($request->session()->get('me'));
$token = $request->session()->get('token'); $user = IndieWebUser::where('me', $url)->firstOrFail();
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint( if ($user->token == null) {
$domain, return redirect(route('micropub-client'))->with('error', 'You havent requested a token yet');
$this->indieClient }
);
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
if (! $micropubEndpoint) { if (! $micropubEndpoint) {
return redirect(route('micropub-client'))->with('error', 'Unable to determine micropub API endpoint'); 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) { if ($user->syntax == 'html') {
$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 didnt create the note.');
}
/**
* Show currently stored configuration values.
*
* @param Illuminate\Http\Request $request
* @return view
*/
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';
return view('micropub.config', compact('data'));
}
/**
* Query the micropub endpoint and store response in the session.
*
* @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) {
try {
$response = $this->guzzleClient->get($micropubEndpoint, [
'headers' => ['Authorization' => 'Bearer ' . $token],
'query' => 'q=config',
]);
} catch (ClientException | ServerException $e) {
return back();
}
$body = (string) $response->getBody();
$syndication = $this->parseSyndicationTargets($body);
$request->session()->put('syndication', $syndication);
$mediaEndpoint = $this->parseMediaEndpoint($body);
$request->session()->put('media-endpoint', $mediaEndpoint);
return back();
}
}
/**
* This method performs the actual POST request.
*
* @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
*/
private function postNoteRequest(
Request $request,
$micropubEndpoint,
$token
) {
$multipart = [ $multipart = [
[ [
'name' => 'h', 'name' => 'h',
@ -246,9 +170,6 @@ class MicropubClientController extends Controller
]; ];
} }
} }
$headers = [
'Authorization' => 'Bearer ' . $token,
];
try { try {
$response = $this->guzzleClient->post($micropubEndpoint, [ $response = $this->guzzleClient->post($micropubEndpoint, [
'multipart' => $multipart, 'multipart' => $multipart,
@ -261,7 +182,208 @@ class MicropubClientController extends Controller
); );
} }
return $response; 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 didnt create the note.');
}
/**
* Show currently stored configuration values.
*
* @param Illuminate\Http\Request $request
* @return view
*/
public function config(Request $request)
{
//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'));
}
/**
* 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> didnt 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)
{
$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],
'query' => 'q=config',
]);
} catch (ClientException | ServerException $e) {
return back();
}
$body = (string) $response->getBody();
$data = json_decode($body, true);
if (array_key_exists('syndicate-to', $data)) {
$user->syndication = json_encode($data['syndicate-to']);
}
if (array_key_exists('media-endpoint', $data)) {
$user->mediaEndpoint = $data['media-endpoint'];
}
$user->save();
return back();
}
}
/**
* Update the syntax setting.
*
* @param Illuminate\Http\Request $request
* @return Illuminate\Http\RedirectResponse
* @todo validate input
*/
public function updateSyntax(Request $request)
{
$user = IndieWebUser::where('me', $request->session()->get('me'))->firstOrFail();
$user->syntax = $request->syntax;
$user->save();
return redirect(route('micropub-config'));
} }
/** /**
@ -272,16 +394,17 @@ class MicropubClientController extends Controller
*/ */
public function newPlace(Request $request) 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([ return response()->json([
'error' => true, 'error' => true,
'error_description' => 'No known token', 'error_description' => 'No known token',
], 400); ], 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) { if (! $micropubEndpoint) {
return response()->json([ return response()->json([
'error' => true, 'error' => true,
@ -289,13 +412,27 @@ class MicropubClientController extends Controller
], 400); ], 400);
} }
$place = $this->postPlaceRequest($request, $micropubEndpoint, $token); $formParams = [
if ($place === false) { '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([ return response()->json([
'error' => true, 'error' => true,
'error_description' => 'Unable to create the new place', 'error_description' => 'Unable to create the new place',
], 400); ], 400);
} }
$place = $response->getHeader('Location')[0];
return response()->json([ return response()->json([
'uri' => $place, '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. * Make a request to the micropub endpoint requesting any nearby places.
* *
@ -351,16 +450,17 @@ class MicropubClientController extends Controller
*/ */
public function nearbyPlaces(Request $request) 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([ return response()->json([
'error' => true, 'error' => true,
'error_description' => 'No known token', 'error_description' => 'No known token',
], 400); ], 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) { if (! $micropubEndpoint) {
return response()->json([ return response()->json([
@ -375,7 +475,7 @@ class MicropubClientController extends Controller
$query .= ';u=' . $request->input('u'); $query .= ';u=' . $request->input('u');
} }
$response = $this->guzzleClient->get($micropubEndpoint, [ $response = $this->guzzleClient->get($micropubEndpoint, [
'headers' => ['Authorization' => 'Bearer ' . $token], 'headers' => ['Authorization' => 'Bearer ' . $user->token],
'query' => ['q' => $query], 'query' => ['q' => $query],
]); ]);
} catch (\GuzzleHttp\Exception\BadResponseException $e) { } catch (\GuzzleHttp\Exception\BadResponseException $e) {
@ -390,31 +490,35 @@ class MicropubClientController extends Controller
} }
/** /**
* Parse the syndication targets retreived from a cookie, to a form that can * Parse the syndication targets JSON into a an array.
* be used in a view.
* *
* @param string $syndicationTargets * @param string|null
* @return array|null * @return array|null
*/ */
private function parseSyndicationTargets($syndicationTargets = null) private function parseSyndicationTargets($syndicationTargets = null)
{ {
if ($syndicationTargets === null) { if ($syndicationTargets === null || $syndicationTargets === '') {
return; return;
} }
$syndicateTo = []; $syndicateTo = [];
$data = json_decode($syndicationTargets, true); $data = json_decode($syndicationTargets, true);
if (array_key_exists('syndicate-to', $data)) { if (array_key_exists('uid', $data)) {
foreach ($data['syndicate-to'] as $syn) { $syndicateTo[] = [
'target' => $data['uid'],
'name' => $data['name'],
];
}
foreach ($data as $syn) {
if (array_key_exists('uid', $syn)) {
$syndicateTo[] = [ $syndicateTo[] = [
'target' => $syn['uid'], 'target' => $syn['uid'],
'name' => $syn['name'], 'name' => $syn['name'],
]; ];
} }
} }
if (count($syndicateTo) > 0) {
return $syndicateTo; return $syndicateTo;
} }
}
/** /**
* Parse the media-endpoint retrieved from querying a micropub endpoint. * Parse the media-endpoint retrieved from querying a micropub endpoint.

View file

@ -3,8 +3,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use App\{Media, Place}; use App\{Media, Note, Place};
use Illuminate\Http\{Request, Response}; use Illuminate\Http\{Request, Response};
use App\Exceptions\InvalidTokenException;
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException; use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
use App\Services\{NoteService, PlaceService, TokenService}; use App\Services\{NoteService, PlaceService, TokenService};
@ -47,29 +48,52 @@ class MicropubController extends Controller
*/ */
public function post(Request $request) public function post(Request $request)
{ {
$httpAuth = $request->header('Authorization'); try {
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) { $tokenData = $this->tokenService->validateToken($request->bearerToken());
$token = $match[1]; } catch (InvalidTokenException $e) {
$tokenData = $this->tokenService->validateToken($token); return response()->json([
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
}
if ($tokenData->hasClaim('scope')) { 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')) { if (($request->input('h') == 'entry') || ($request->input('type')[0] == 'h-entry')) {
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->returnInsufficientScopeResponse();
}
$data = []; $data = [];
$data['client-id'] = $clientId; $data['client-id'] = $tokenData->getClaim('client_id');
if ($request->header('Content-Type') == 'application/json') { if ($request->header('Content-Type') == 'application/json') {
$data['content'] = $request->input('properties.content')[0]; 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['in-reply-to'] = $request->input('properties.in-reply-to')[0];
$data['location'] = $request->input('properties.location'); $data['location'] = $request->input('properties.location');
//flatten location if array //flatten location if array
if (is_array($data['location'])) { if (is_array($data['location'])) {
$data['location'] = $data['location'][0]; $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;
}
}
} else { } else {
$data['content'] = $request->input('content'); $data['content'] = $request->input('content');
$data['in-reply-to'] = $request->input('in-reply-to'); $data['in-reply-to'] = $request->input('in-reply-to');
$data['location'] = $request->input('location'); $data['location'] = $request->input('location');
$data['published'] = $request->input('published');
} }
$data['syndicate'] = []; $data['syndicate'] = [];
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name'); $targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
@ -105,7 +129,7 @@ class MicropubController extends Controller
} }
try { try {
$note = $this->noteService->createNote($data); $note = $this->noteService->createNote($data);
} catch (Exception $exception) { } catch (\Exception $exception) {
return response()->json(['error' => true], 400); return response()->json(['error' => true], 400);
} }
@ -115,6 +139,9 @@ class MicropubController extends Controller
], 201)->header('Location', $note->longurl); ], 201)->header('Location', $note->longurl);
} }
if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') { if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') {
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->returnInsufficientScopeResponse();
}
$data = []; $data = [];
if ($request->header('Content-Type') == 'application/json') { if ($request->header('Content-Type') == 'application/json') {
$data['name'] = $request->input('properties.name'); $data['name'] = $request->input('properties.name');
@ -135,7 +162,7 @@ class MicropubController extends Controller
} }
try { try {
$place = $this->placeService->createPlace($data); $place = $this->placeService->createPlace($data);
} catch (Exception $exception) { } catch (\Exception $exception) {
return response()->json(['error' => true], 400); return response()->json(['error' => true], 400);
} }
@ -144,21 +171,90 @@ class MicropubController extends Controller
'location' => $place->longurl, 'location' => $place->longurl,
], 201)->header('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];
}
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));
}
}
}
}
$note->save();
return response()->json([
'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([ return response()->json([
'response' => 'error', 'response' => 'error',
'error' => 'invalid_token', 'error' => 'forbidden',
'error_description' => 'The token provided is not valid or does not have the necessary scope', 'error_description' => 'The token has no scopes',
], 400); ], 403);
}
return response()->json([
'response' => 'error',
'error' => 'no_token',
'error_description' => 'No OAuth token sent with request',
], 400);
} }
/** /**
@ -172,12 +268,9 @@ class MicropubController extends Controller
*/ */
public function get(Request $request) public function get(Request $request)
{ {
$httpAuth = $request->header('Authorization'); try {
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) { $tokenData = $this->tokenService->validateToken($request->bearerToken());
$token = $match[1]; } catch (InvalidTokenException $e) {
$valid = $this->tokenService->validateToken($token);
if ($valid === null) {
return response()->json([ return response()->json([
'response' => 'error', 'response' => 'error',
'error' => 'invalid_token', 'error' => 'invalid_token',
@ -222,20 +315,13 @@ class MicropubController extends Controller
return response()->json([ return response()->json([
'response' => 'token', 'response' => 'token',
'token' => [ 'token' => [
'me' => $valid->getClaim('me'), 'me' => $tokenData->getClaim('me'),
'scope' => $valid->getClaim('scope'), 'scope' => $tokenData->getClaim('scope'),
'client_id' => $valid->getClaim('client_id'), 'client_id' => $tokenData->getClaim('client_id'),
], ],
]); ]);
} }
return response()->json([
'response' => 'error',
'error' => 'no_token',
'error_description' => 'No token provided with request',
], 400);
}
/** /**
* Process a media item posted to the media endpoint. * Process a media item posted to the media endpoint.
* *
@ -244,13 +330,9 @@ class MicropubController extends Controller
*/ */
public function media(Request $request) public function media(Request $request)
{ {
//can this go in middleware try {
$httpAuth = $request->header('Authorization'); $tokenData = $this->tokenService->validateToken($request->bearerToken());
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) { } catch (InvalidTokenException $e) {
$token = $match[1];
$tokenData = $this->tokenService->validateToken($token);
if ($tokenData === null) {
return response()->json([ return response()->json([
'response' => 'error', 'response' => 'error',
'error' => 'invalid_token', 'error' => 'invalid_token',
@ -260,8 +342,9 @@ class MicropubController extends Controller
//check post scope //check post scope
if ($tokenData->hasClaim('scope')) { if ($tokenData->hasClaim('scope')) {
$scopes = explode(' ', $tokenData->getClaim('scope')); if (stristr($token->getClaim('scope'), 'post') === false) {
if (array_search('post', $scopes) !== false) { return $this->returnInsufficientScopeResponse();
}
//check media valid //check media valid
if ($request->hasFile('file') && $request->file('file')->isValid()) { if ($request->hasFile('file') && $request->file('file')->isValid()) {
$type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType()); $type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType());
@ -284,7 +367,7 @@ class MicropubController extends Controller
], 503); ], 503);
} }
$media = new Media(); $media = new Media();
$media->token = $token; $media->token = $request->bearerToken();
$media->path = $path; $media->path = $path;
$media->type = $type; $media->type = $type;
$media->save(); $media->save();
@ -304,22 +387,8 @@ class MicropubController extends Controller
return response()->json([ return response()->json([
'response' => 'error', 'response' => 'error',
'error' => 'insufficient_scope', 'error' => 'invalid_request',
'error_description' => 'The provided token has insufficient scopes', 'error_description' => 'The provided token has no scopes',
], 401);
}
return response()->json([
'response' => 'error',
'error' => 'unauthorized',
'error_description' => 'No token provided with request',
], 401);
}
return response()->json([
'response' => 'error',
'error' => 'no_token',
'error_description' => 'There was no token provided with the request',
], 400); ], 400);
} }
@ -366,4 +435,13 @@ class MicropubController extends Controller
return 'download'; return 'download';
} }
private function returnInsufficientScopeResponse()
{
return response()->json([
'response' => 'error',
'error' => 'insufficient_scope',
'error_description' => 'The tokens scope does not have the necessary requirements.',
], 401);
}
} }

View 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 authd 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('Cant determine the authorisation endpoint.', 400);
}
}

View file

@ -34,7 +34,8 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\LinkHeadersMiddleware::class, \App\Http\Middleware\LinkHeadersMiddleware::class,
\App\Http\Middleware\DevTokenMiddleware::class, //\App\Http\Middleware\DevTokenMiddleware::class,
\App\Http\Middleware\LocalhostSessionMiddleware::class,
], ],
'api' => [ 'api' => [
@ -58,5 +59,6 @@ class Kernel extends HttpKernel
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'myauth' => \App\Http\Middleware\MyAuthMiddleware::class, 'myauth' => \App\Http\Middleware\MyAuthMiddleware::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'micropub.token' => \App\Http\Middleware\VerifyMicropubToken::class,
]; ];
} }

View 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 cant 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);
}
}

View 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
View 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'];
}

View file

@ -28,6 +28,10 @@ class Media extends Model
*/ */
public function getUrlAttribute() public function getUrlAttribute()
{ {
if (starts_with($this->path, 'https://')) {
return $this->path;
}
return config('filesystems.disks.s3.url') . '/' . $this->path; return config('filesystems.disks.s3.url') . '/' . $this->path;
} }
} }

View file

@ -191,6 +191,20 @@ class Note extends Model
return $name; 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)!) * 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 * we try to create a fancy hcard from our contact info. If this is not possible

View file

@ -33,7 +33,7 @@ class AppServiceProvider extends ServiceProvider
//Add tags for notes //Add tags for notes
Note::created(function ($note) { Note::created(function ($note) {
$tagsToAdd = []; $tagsToAdd = [];
preg_match_all('/#([^\s<>]+)\b/', $note, $tags); preg_match_all('/#([^\s<>]+)\b/', $note->note, $tags);
foreach ($tags[1] as $tag) { foreach ($tags[1] as $tag) {
$tag = Tag::normalizeTag($tag); $tag = Tag::normalizeTag($tag);
} }

View file

@ -13,7 +13,7 @@ class EventServiceProvider extends ServiceProvider
* @var array * @var array
*/ */
protected $listen = [ protected $listen = [
'App\Events\SomeEvent' => [ 'App\Events\Event' => [
'App\Listeners\EventListener', 'App\Listeners\EventListener',
], ],
]; ];

View file

@ -45,14 +45,12 @@ class IndieAuthService
session(['state' => $state]); session(['state' => $state]);
$redirectURL = route('indieauth-callback'); $redirectURL = route('indieauth-callback');
$clientId = route('micropub-client'); $clientId = route('micropub-client');
$scope = 'post';
$authorizationURL = $this->client->buildAuthorizationURL( $authorizationURL = $this->client->buildAuthorizationURL(
$authEndpoint, $authEndpoint,
$this->client->normalizeMeURL($domain), $this->client->normalizeMeURL($domain),
$redirectURL, $redirectURL,
$clientId, $clientId,
$state, $state
$scope
); );
return $authorizationURL; return $authorizationURL;

View file

@ -17,6 +17,16 @@ class NoteService
*/ */
public function createNote(array $data): Note public function createNote(array $data): Note
{ {
//check the input
if (array_key_exists('content', $data) === false) {
throw new \Exception('No content defined'); //we cant 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 = Note::create(
[ [
'note' => $data['content'], '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 (array_key_exists('location', $data) && $data['location'] !== null && $data['location'] !== 'no-location') {
if (starts_with($data['location'], config('app.url'))) { if (starts_with($data['location'], config('app.url'))) {
//uri of form http://host/places/slug, we want slug //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 /* drop image support for now
//add images to media library //add images to media library
if ($request->hasFile('photo')) { if ($request->hasFile('photo')) {
@ -55,12 +77,17 @@ class NoteService
*/ */
//add support for media uploaded as URLs //add support for media uploaded as URLs
foreach ($data['photo'] as $photo) { 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'))) { if (starts_with($photo, config('filesystems.disks.s3.url'))) {
$path = substr($photo, strlen(config('filesystems.disks.s3.url'))); $path = substr($photo, strlen(config('filesystems.disks.s3.url')));
$media = Media::where('path', ltrim($path, '/'))->firstOrFail(); $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(); $note->save();

View file

@ -36,4 +36,39 @@ class PlaceService
return $place; 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;
}
} }

View file

@ -4,12 +4,9 @@ declare(strict_types=1);
namespace App\Services; namespace App\Services;
use RuntimeException;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Builder;
use InvalidArgumentException;
use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Hmac\Sha256;
use App\Exceptions\InvalidTokenException;
use Lcobucci\JWT\{Builder, Parser, Token};
class TokenService class TokenService
{ {
@ -39,17 +36,18 @@ class TokenService
* @param string The token * @param string The token
* @return mixed * @return mixed
*/ */
public function validateToken(string $token): ?Token public function validateToken(string $bearerToken): ?Token
{ {
$signer = new Sha256(); $signer = new Sha256();
try { try {
$token = (new Parser())->parse((string) $token); $token = (new Parser())->parse((string) $bearerToken);
} catch (InvalidArgumentException | RuntimeException $e) { } catch (\InvalidArgumentException $e) {
return null; throw new InvalidTokenException('Token could not be parsed');
} }
if ($token->verify($signer, config('app.key'))) { if (! $token->verify($signer, config('app.key'))) {
//signuture valid throw new InvalidTokenException('Token failed verification');
}
return $token; return $token;
} }
}
} }

View file

@ -1,5 +1,10 @@
# Changelog # Changelog
## Version 0.5 (2017-06-18)
- Update micropub client to allow indieweb users
- Update micropub endpoint to allow for entry updates
- Add support for checkins, so we can use ownyourswarm
## Version 0.4.2 (2017-03-24) ## Version 0.4.2 (2017-03-24)
- fixed issue#47, only the slug was being sent by client, which was messing up endpoint code - fixed issue#47, only the slug was being sent by client, which was messing up endpoint code
- minor changes to es6 code, bet lint-staged working again - minor changes to es6 code, bet lint-staged working again

View file

@ -42,7 +42,10 @@
], ],
"psr-4": { "psr-4": {
"App\\": "app/" "App\\": "app/"
} },
"files": [
"helpers.php"
]
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
@ -67,6 +70,7 @@
}, },
"config": { "config": {
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true "sort-packages": true,
"optimize-autoloader": true
} }
} }

381
composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [ "packages": [
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.24.7", "version": "3.27.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "f062d7ea2123fe2aefef91da855c10ef8ff3af1c" "reference": "eb10e43cccf8e868f9622ab8ce2beb9fb756b5a8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f062d7ea2123fe2aefef91da855c10ef8ff3af1c", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eb10e43cccf8e868f9622ab8ce2beb9fb756b5a8",
"reference": "f062d7ea2123fe2aefef91da855c10ef8ff3af1c", "reference": "eb10e43cccf8e868f9622ab8ce2beb9fb756b5a8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -39,7 +39,7 @@
"ext-simplexml": "*", "ext-simplexml": "*",
"ext-spl": "*", "ext-spl": "*",
"nette/neon": "^2.3", "nette/neon": "^2.3",
"phpunit/phpunit": "~4.0|~5.0", "phpunit/phpunit": "^4.8.35|^5.4.0",
"psr/cache": "^1.0" "psr/cache": "^1.0"
}, },
"suggest": { "suggest": {
@ -84,7 +84,7 @@
"s3", "s3",
"sdk" "sdk"
], ],
"time": "2017-03-23T22:17:20+00:00" "time": "2017-05-11T21:23:43+00:00"
}, },
{ {
"name": "barnabywalters/mf-cleaner", "name": "barnabywalters/mf-cleaner",
@ -180,6 +180,65 @@
], ],
"time": "2016-08-19T16:43:44+00:00" "time": "2016-08-19T16:43:44+00:00"
}, },
{
"name": "composer/ca-bundle",
"version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
"reference": "b17e6153cb7f33c7e44eb59578dc12eee5dc8e12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/b17e6153cb7f33c7e44eb59578dc12eee5dc8e12",
"reference": "b17e6153cb7f33c7e44eb59578dc12eee5dc8e12",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-pcre": "*",
"php": "^5.3.2 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.5",
"psr/log": "^1.0",
"symfony/process": "^2.5 || ^3.0"
},
"suggest": {
"symfony/process": "This is necessary to reliably check whether openssl_x509_parse is vulnerable on older php versions, but can be ignored on PHP 5.5.6+"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\CaBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
"keywords": [
"cabundle",
"cacert",
"certificate",
"ssl",
"tls"
],
"time": "2017-03-06T11:59:08+00:00"
},
{ {
"name": "dnoegel/php-xdg-base-dir", "name": "dnoegel/php-xdg-base-dir",
"version": "0.1", "version": "0.1",
@ -685,16 +744,16 @@
}, },
{ {
"name": "erusev/parsedown", "name": "erusev/parsedown",
"version": "1.6.1", "version": "1.6.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/erusev/parsedown.git", "url": "https://github.com/erusev/parsedown.git",
"reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb" "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/erusev/parsedown/zipball/20ff8bbb57205368b4b42d094642a3e52dac85fb", "url": "https://api.github.com/repos/erusev/parsedown/zipball/1bf24f7334fe16c88bf9d467863309ceaf285b01",
"reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb", "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -723,7 +782,7 @@
"markdown", "markdown",
"parser" "parser"
], ],
"time": "2016-11-02T15:56:58+00:00" "time": "2017-03-29T16:04:15+00:00"
}, },
{ {
"name": "ezyang/htmlpurifier", "name": "ezyang/htmlpurifier",
@ -1045,16 +1104,16 @@
}, },
{ {
"name": "indieauth/client", "name": "indieauth/client",
"version": "0.2.0", "version": "0.2.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/indieweb/indieauth-client-php.git", "url": "https://github.com/indieweb/indieauth-client-php.git",
"reference": "4b9bd766a92b8abbe420f5889bf7ebac7678151d" "reference": "f5f6efad79334d1ff9370fe4dce8ccf4814820fa"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/indieweb/indieauth-client-php/zipball/4b9bd766a92b8abbe420f5889bf7ebac7678151d", "url": "https://api.github.com/repos/indieweb/indieauth-client-php/zipball/f5f6efad79334d1ff9370fe4dce8ccf4814820fa",
"reference": "4b9bd766a92b8abbe420f5889bf7ebac7678151d", "reference": "f5f6efad79334d1ff9370fe4dce8ccf4814820fa",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1080,7 +1139,7 @@
} }
], ],
"description": "IndieAuth Client Library", "description": "IndieAuth Client Library",
"time": "2017-02-09T23:42:05+00:00" "time": "2017-04-26T21:44:35+00:00"
}, },
{ {
"name": "indieweb/link-rel-parser", "name": "indieweb/link-rel-parser",
@ -1405,16 +1464,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v5.4.16", "version": "v5.4.23",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "6cf379ec34d08bcdc9c7183e369a8fdf04ade80d" "reference": "ad82327705658dbf5f0ce72805caa950dfbe150d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/6cf379ec34d08bcdc9c7183e369a8fdf04ade80d", "url": "https://api.github.com/repos/laravel/framework/zipball/ad82327705658dbf5f0ce72805caa950dfbe150d",
"reference": "6cf379ec34d08bcdc9c7183e369a8fdf04ade80d", "reference": "ad82327705658dbf5f0ce72805caa950dfbe150d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1530,20 +1589,20 @@
"framework", "framework",
"laravel" "laravel"
], ],
"time": "2017-03-21T19:34:41+00:00" "time": "2017-05-11T20:10:35+00:00"
}, },
{ {
"name": "laravel/scout", "name": "laravel/scout",
"version": "v3.0.2", "version": "v3.0.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/scout.git", "url": "https://github.com/laravel/scout.git",
"reference": "1ddb0fa6f165bf6a69864960102062e7cf3f989d" "reference": "64d28db58a054174eadf1d4df38dad81ff7e68dd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/scout/zipball/1ddb0fa6f165bf6a69864960102062e7cf3f989d", "url": "https://api.github.com/repos/laravel/scout/zipball/64d28db58a054174eadf1d4df38dad81ff7e68dd",
"reference": "1ddb0fa6f165bf6a69864960102062e7cf3f989d", "reference": "64d28db58a054174eadf1d4df38dad81ff7e68dd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1590,7 +1649,7 @@
"laravel", "laravel",
"search" "search"
], ],
"time": "2017-03-01T14:37:40+00:00" "time": "2017-04-09T00:54:26+00:00"
}, },
{ {
"name": "laravel/tinker", "name": "laravel/tinker",
@ -1710,16 +1769,16 @@
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
"version": "0.15.3", "version": "0.15.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/commonmark.git", "url": "https://github.com/thephpleague/commonmark.git",
"reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc" "reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c8b43ee5821362216f8e9ac684f0f59de164edcc", "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c",
"reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc", "reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1775,20 +1834,20 @@
"markdown", "markdown",
"parser" "parser"
], ],
"time": "2016-12-19T00:11:43+00:00" "time": "2017-05-09T12:47:53+00:00"
}, },
{ {
"name": "league/flysystem", "name": "league/flysystem",
"version": "1.0.37", "version": "1.0.40",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem.git", "url": "https://github.com/thephpleague/flysystem.git",
"reference": "78b5cc4feb61a882302df4fbaf63b7662e5e4ccd" "reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/78b5cc4feb61a882302df4fbaf63b7662e5e4ccd", "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3828f0b24e2c1918bb362d57a53205d6dc8fde61",
"reference": "78b5cc4feb61a882302df4fbaf63b7662e5e4ccd", "reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1810,12 +1869,12 @@
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage", "league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
"league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
"league/flysystem-copy": "Allows you to use Copy.com storage", "league/flysystem-copy": "Allows you to use Copy.com storage",
"league/flysystem-dropbox": "Allows you to use Dropbox storage",
"league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
"league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
"league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
"league/flysystem-webdav": "Allows you to use WebDAV storage", "league/flysystem-webdav": "Allows you to use WebDAV storage",
"league/flysystem-ziparchive": "Allows you to use ZipArchive adapter" "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -1858,25 +1917,25 @@
"sftp", "sftp",
"storage" "storage"
], ],
"time": "2017-03-22T15:43:14+00:00" "time": "2017-04-28T10:15:08+00:00"
}, },
{ {
"name": "league/flysystem-aws-s3-v3", "name": "league/flysystem-aws-s3-v3",
"version": "1.0.13", "version": "1.0.15",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "dc56a8faf3aff0841f9eae04b6af94a50657896c" "reference": "c947f36f977b495a57e857ae1630df0da35ec456"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/dc56a8faf3aff0841f9eae04b6af94a50657896c", "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c947f36f977b495a57e857ae1630df0da35ec456",
"reference": "dc56a8faf3aff0841f9eae04b6af94a50657896c", "reference": "c947f36f977b495a57e857ae1630df0da35ec456",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"aws/aws-sdk-php": "^3.0.0", "aws/aws-sdk-php": "^3.0.0",
"league/flysystem": "~1.0", "league/flysystem": "^1.0.40",
"php": ">=5.5.0" "php": ">=5.5.0"
}, },
"require-dev": { "require-dev": {
@ -1905,7 +1964,7 @@
} }
], ],
"description": "Flysystem adapter for the AWS S3 SDK v3.x", "description": "Flysystem adapter for the AWS S3 SDK v3.x",
"time": "2016-06-21T21:34:35+00:00" "time": "2017-04-28T10:21:54+00:00"
}, },
{ {
"name": "martinbean/laravel-sluggable-trait", "name": "martinbean/laravel-sluggable-trait",
@ -2658,16 +2717,16 @@
}, },
{ {
"name": "ramsey/uuid", "name": "ramsey/uuid",
"version": "3.6.0", "version": "3.6.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/ramsey/uuid.git", "url": "https://github.com/ramsey/uuid.git",
"reference": "0b7bdfb180e72c8d76e75a649ced67e392201458" "reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/0b7bdfb180e72c8d76e75a649ced67e392201458", "url": "https://api.github.com/repos/ramsey/uuid/zipball/4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"reference": "0b7bdfb180e72c8d76e75a649ced67e392201458", "reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2679,9 +2738,9 @@
}, },
"require-dev": { "require-dev": {
"apigen/apigen": "^4.1", "apigen/apigen": "^4.1",
"codeception/aspect-mock": "1.0.0", "codeception/aspect-mock": "^1.0 | ^2.0",
"doctrine/annotations": "~1.2.0", "doctrine/annotations": "~1.2.0",
"goaop/framework": "1.0.0-alpha.2", "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1",
"ircmaxell/random-lib": "^1.1", "ircmaxell/random-lib": "^1.1",
"jakub-onderka/php-parallel-lint": "^0.9.0", "jakub-onderka/php-parallel-lint": "^0.9.0",
"mockery/mockery": "^0.9.4", "mockery/mockery": "^0.9.4",
@ -2736,23 +2795,24 @@
"identifier", "identifier",
"uuid" "uuid"
], ],
"time": "2017-03-18T15:38:09+00:00" "time": "2017-03-26T20:37:53+00:00"
}, },
{ {
"name": "sensiolabs/security-checker", "name": "sensiolabs/security-checker",
"version": "v4.0.2", "version": "v4.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sensiolabs/security-checker.git", "url": "https://github.com/sensiolabs/security-checker.git",
"reference": "56bded66985e22f6eac2cf86735fd21c625bff2f" "reference": "9e69eddf3bc49d1ee5c7908564da3141796d4bbc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sensiolabs/security-checker/zipball/56bded66985e22f6eac2cf86735fd21c625bff2f", "url": "https://api.github.com/repos/sensiolabs/security-checker/zipball/9e69eddf3bc49d1ee5c7908564da3141796d4bbc",
"reference": "56bded66985e22f6eac2cf86735fd21c625bff2f", "reference": "9e69eddf3bc49d1ee5c7908564da3141796d4bbc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"composer/ca-bundle": "^1.0",
"symfony/console": "~2.7|~3.0" "symfony/console": "~2.7|~3.0"
}, },
"bin": [ "bin": [
@ -2780,20 +2840,20 @@
} }
], ],
"description": "A security checker for your composer.lock", "description": "A security checker for your composer.lock",
"time": "2017-03-09T17:33:20+00:00" "time": "2017-03-31T14:50:32+00:00"
}, },
{ {
"name": "swiftmailer/swiftmailer", "name": "swiftmailer/swiftmailer",
"version": "v5.4.6", "version": "v5.4.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git", "url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e" "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e", "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/9a06dc570a0367850280eefd3f1dc2da45aef517",
"reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e", "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2834,20 +2894,20 @@
"mail", "mail",
"mailer" "mailer"
], ],
"time": "2017-02-13T07:52:53+00:00" "time": "2017-05-01T15:54:03+00:00"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "28fb243a2b5727774ca309ec2d92da240f1af0dd" "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/28fb243a2b5727774ca309ec2d92da240f1af0dd", "url": "https://api.github.com/repos/symfony/console/zipball/a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38",
"reference": "28fb243a2b5727774ca309ec2d92da240f1af0dd", "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2897,20 +2957,20 @@
], ],
"description": "Symfony Console Component", "description": "Symfony Console Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-03-06T19:30:27+00:00" "time": "2017-04-26T01:39:17+00:00"
}, },
{ {
"name": "symfony/css-selector", "name": "symfony/css-selector",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/css-selector.git", "url": "https://github.com/symfony/css-selector.git",
"reference": "a48f13dc83c168f1253a5d2a5a4fb46c36244c4c" "reference": "02983c144038e697c959e6b06ef6666de759ccbc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/a48f13dc83c168f1253a5d2a5a4fb46c36244c4c", "url": "https://api.github.com/repos/symfony/css-selector/zipball/02983c144038e697c959e6b06ef6666de759ccbc",
"reference": "a48f13dc83c168f1253a5d2a5a4fb46c36244c4c", "reference": "02983c144038e697c959e6b06ef6666de759ccbc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2950,20 +3010,20 @@
], ],
"description": "Symfony CssSelector Component", "description": "Symfony CssSelector Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-02-21T09:12:04+00:00" "time": "2017-05-01T14:55:58+00:00"
}, },
{ {
"name": "symfony/debug", "name": "symfony/debug",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/debug.git", "url": "https://github.com/symfony/debug.git",
"reference": "b90c9f91ad8ac37d9f114e369042d3226b34dc1a" "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/b90c9f91ad8ac37d9f114e369042d3226b34dc1a", "url": "https://api.github.com/repos/symfony/debug/zipball/fd6eeee656a5a7b384d56f1072243fe1c0e81686",
"reference": "b90c9f91ad8ac37d9f114e369042d3226b34dc1a", "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3007,20 +3067,20 @@
], ],
"description": "Symfony Debug Component", "description": "Symfony Debug Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-02-18T17:28:00+00:00" "time": "2017-04-19T20:17:50+00:00"
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher.git", "url": "https://github.com/symfony/event-dispatcher.git",
"reference": "b7a1b9e0a0f623ce43b4c8d775eb138f190c9d8d" "reference": "b8a401f733b43251e1d088c589368b2a94155e40"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7a1b9e0a0f623ce43b4c8d775eb138f190c9d8d", "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b8a401f733b43251e1d088c589368b2a94155e40",
"reference": "b7a1b9e0a0f623ce43b4c8d775eb138f190c9d8d", "reference": "b8a401f733b43251e1d088c589368b2a94155e40",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3067,20 +3127,20 @@
], ],
"description": "Symfony EventDispatcher Component", "description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-02-21T09:12:04+00:00" "time": "2017-05-01T14:58:48+00:00"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "92d7476d2df60cd851a3e13e078664b1deb8ce10" "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/92d7476d2df60cd851a3e13e078664b1deb8ce10", "url": "https://api.github.com/repos/symfony/finder/zipball/9cf076f8f492f4b1ffac40aae9c2d287b4ca6930",
"reference": "92d7476d2df60cd851a3e13e078664b1deb8ce10", "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3116,20 +3176,20 @@
], ],
"description": "Symfony Finder Component", "description": "Symfony Finder Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-02-21T09:12:04+00:00" "time": "2017-04-12T14:13:17+00:00"
}, },
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-foundation.git", "url": "https://github.com/symfony/http-foundation.git",
"reference": "c57009887010eb4e58bfca2970314a5b820b24b9" "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/c57009887010eb4e58bfca2970314a5b820b24b9", "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9de6add7f731e5af7f5b2e9c0da365e43383ebef",
"reference": "c57009887010eb4e58bfca2970314a5b820b24b9", "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3169,20 +3229,20 @@
], ],
"description": "Symfony HttpFoundation Component", "description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-03-04T12:23:14+00:00" "time": "2017-05-01T14:55:58+00:00"
}, },
{ {
"name": "symfony/http-kernel", "name": "symfony/http-kernel",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-kernel.git", "url": "https://github.com/symfony/http-kernel.git",
"reference": "bc909e85b8585c9edf043d0fca871308c41bb9b4" "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/bc909e85b8585c9edf043d0fca871308c41bb9b4", "url": "https://api.github.com/repos/symfony/http-kernel/zipball/46e8b209abab55c072c47d72d5cd1d62c0585e05",
"reference": "bc909e85b8585c9edf043d0fca871308c41bb9b4", "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3251,7 +3311,7 @@
], ],
"description": "Symfony HttpKernel Component", "description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-03-10T18:35:31+00:00" "time": "2017-05-01T17:46:48+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
@ -3314,16 +3374,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "68bfa8c83f24c0ac04ea7193bcdcda4519f41892" "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/68bfa8c83f24c0ac04ea7193bcdcda4519f41892", "url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0",
"reference": "68bfa8c83f24c0ac04ea7193bcdcda4519f41892", "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3359,20 +3419,20 @@
], ],
"description": "Symfony Process Component", "description": "Symfony Process Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-03-04T12:23:14+00:00" "time": "2017-04-12T14:13:17+00:00"
}, },
{ {
"name": "symfony/routing", "name": "symfony/routing",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/routing.git", "url": "https://github.com/symfony/routing.git",
"reference": "d6605f9a5767bc5bc4895e1c762ba93964608aee" "reference": "5029745d6d463585e8b487dbc83d6333f408853a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/d6605f9a5767bc5bc4895e1c762ba93964608aee", "url": "https://api.github.com/repos/symfony/routing/zipball/5029745d6d463585e8b487dbc83d6333f408853a",
"reference": "d6605f9a5767bc5bc4895e1c762ba93964608aee", "reference": "5029745d6d463585e8b487dbc83d6333f408853a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3434,20 +3494,20 @@
"uri", "uri",
"url" "url"
], ],
"time": "2017-03-02T15:58:09+00:00" "time": "2017-04-12T14:13:17+00:00"
}, },
{ {
"name": "symfony/translation", "name": "symfony/translation",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/translation.git", "url": "https://github.com/symfony/translation.git",
"reference": "0e1b15ce8fbf3890f4ccdac430ed5e07fdfe0690" "reference": "f4a04d2df710f81515df576b2de06bdeee518b83"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/0e1b15ce8fbf3890f4ccdac430ed5e07fdfe0690", "url": "https://api.github.com/repos/symfony/translation/zipball/f4a04d2df710f81515df576b2de06bdeee518b83",
"reference": "0e1b15ce8fbf3890f4ccdac430ed5e07fdfe0690", "reference": "f4a04d2df710f81515df576b2de06bdeee518b83",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3498,20 +3558,20 @@
], ],
"description": "Symfony Translation Component", "description": "Symfony Translation Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-03-04T12:23:14+00:00" "time": "2017-04-12T14:13:17+00:00"
}, },
{ {
"name": "symfony/var-dumper", "name": "symfony/var-dumper",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-dumper.git", "url": "https://github.com/symfony/var-dumper.git",
"reference": "4100f347aff890bc16b0b4b42843b599db257b2d" "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/4100f347aff890bc16b0b4b42843b599db257b2d", "url": "https://api.github.com/repos/symfony/var-dumper/zipball/fa47963ac7979ddbd42b2d646d1b056bddbf7bb8",
"reference": "4100f347aff890bc16b0b4b42843b599db257b2d", "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3522,9 +3582,11 @@
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
}, },
"require-dev": { "require-dev": {
"ext-iconv": "*",
"twig/twig": "~1.20|~2.0" "twig/twig": "~1.20|~2.0"
}, },
"suggest": { "suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
"ext-symfony_debug": "" "ext-symfony_debug": ""
}, },
"type": "library", "type": "library",
@ -3564,7 +3626,7 @@
"debug", "debug",
"dump" "dump"
], ],
"time": "2017-02-20T13:45:48+00:00" "time": "2017-05-01T14:55:58+00:00"
}, },
{ {
"name": "themattharris/tmhoauth", "name": "themattharris/tmhoauth",
@ -3610,16 +3672,16 @@
}, },
{ {
"name": "thujohn/twitter", "name": "thujohn/twitter",
"version": "2.2.2", "version": "2.2.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thujohn/twitter.git", "url": "https://github.com/thujohn/twitter.git",
"reference": "203d903856212835206675ae9c0504d74b681886" "reference": "ff414bdadba3f1570ca211355e5359ec266552d8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thujohn/twitter/zipball/203d903856212835206675ae9c0504d74b681886", "url": "https://api.github.com/repos/thujohn/twitter/zipball/ff414bdadba3f1570ca211355e5359ec266552d8",
"reference": "203d903856212835206675ae9c0504d74b681886", "reference": "ff414bdadba3f1570ca211355e5359ec266552d8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3650,7 +3712,7 @@
"laravel5", "laravel5",
"twitter" "twitter"
], ],
"time": "2017-01-31T23:37:52+00:00" "time": "2017-04-27T09:00:04+00:00"
}, },
{ {
"name": "tijsverkoyen/css-to-inline-styles", "name": "tijsverkoyen/css-to-inline-styles",
@ -3861,16 +3923,16 @@
}, },
{ {
"name": "facebook/webdriver", "name": "facebook/webdriver",
"version": "1.4.0", "version": "1.4.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/facebook/php-webdriver.git", "url": "https://github.com/facebook/php-webdriver.git",
"reference": "3ea034c056189e11c0ce7985332a9f4b5b2b5db2" "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/facebook/php-webdriver/zipball/3ea034c056189e11c0ce7985332a9f4b5b2b5db2", "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/eadb0b7a7c3e6578185197fd40158b08c3164c83",
"reference": "3ea034c056189e11c0ce7985332a9f4b5b2b5db2", "reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3909,7 +3971,7 @@
"selenium", "selenium",
"webdriver" "webdriver"
], ],
"time": "2017-03-22T10:56:03+00:00" "time": "2017-04-28T14:54:49+00:00"
}, },
{ {
"name": "fzaninotto/faker", "name": "fzaninotto/faker",
@ -4053,21 +4115,22 @@
}, },
{ {
"name": "laravel/dusk", "name": "laravel/dusk",
"version": "v1.0.10", "version": "v1.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/dusk.git", "url": "https://github.com/laravel/dusk.git",
"reference": "11537ac1a939a2194e9e3cdc2536e6e34eff9ea6" "reference": "6b81e97ae1ce384e3d8dbd020b2b9751c1449889"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/dusk/zipball/11537ac1a939a2194e9e3cdc2536e6e34eff9ea6", "url": "https://api.github.com/repos/laravel/dusk/zipball/6b81e97ae1ce384e3d8dbd020b2b9751c1449889",
"reference": "11537ac1a939a2194e9e3cdc2536e6e34eff9ea6", "reference": "6b81e97ae1ce384e3d8dbd020b2b9751c1449889",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"facebook/webdriver": "~1.0", "facebook/webdriver": "~1.0",
"illuminate/support": "~5.3", "illuminate/console": "~5.4",
"illuminate/support": "~5.4",
"nesbot/carbon": "~1.20", "nesbot/carbon": "~1.20",
"php": ">=5.6.4", "php": ">=5.6.4",
"symfony/console": "~3.2", "symfony/console": "~3.2",
@ -4104,7 +4167,7 @@
"testing", "testing",
"webdriver" "webdriver"
], ],
"time": "2017-03-03T14:36:19+00:00" "time": "2017-04-23T17:13:04+00:00"
}, },
{ {
"name": "maximebf/debugbar", "name": "maximebf/debugbar",
@ -4172,12 +4235,12 @@
"version": "0.9.9", "version": "0.9.9",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/padraic/mockery.git", "url": "https://github.com/mockery/mockery.git",
"reference": "6fdb61243844dc924071d3404bb23994ea0b6856" "reference": "6fdb61243844dc924071d3404bb23994ea0b6856"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/padraic/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856", "url": "https://api.github.com/repos/mockery/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856",
"reference": "6fdb61243844dc924071d3404bb23994ea0b6856", "reference": "6fdb61243844dc924071d3404bb23994ea0b6856",
"shasum": "" "shasum": ""
}, },
@ -4234,16 +4297,16 @@
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.6.0", "version": "1.6.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe" "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102",
"reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe", "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4272,7 +4335,7 @@
"object", "object",
"object graph" "object graph"
], ],
"time": "2017-01-26T22:05:40+00:00" "time": "2017-04-12T18:52:22+00:00"
}, },
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
@ -4485,16 +4548,16 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "4.0.7", "version": "4.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "09e2277d14ea467e5a984010f501343ef29ffc69" "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/09e2277d14ea467e5a984010f501343ef29ffc69", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
"reference": "09e2277d14ea467e5a984010f501343ef29ffc69", "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4544,7 +4607,7 @@
"testing", "testing",
"xunit" "xunit"
], ],
"time": "2017-03-01T09:12:17+00:00" "time": "2017-04-02T07:44:40+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@ -4734,16 +4797,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "5.7.17", "version": "5.7.19",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "68752b665d3875f9a38a357e3ecb35c79f8673bf" "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/68752b665d3875f9a38a357e3ecb35c79f8673bf", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1",
"reference": "68752b665d3875f9a38a357e3ecb35c79f8673bf", "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4812,7 +4875,7 @@
"testing", "testing",
"xunit" "xunit"
], ],
"time": "2017-03-19T16:52:12+00:00" "time": "2017-04-03T02:22:27+00:00"
}, },
{ {
"name": "phpunit/phpunit-mock-objects", "name": "phpunit/phpunit-mock-objects",
@ -5477,16 +5540,16 @@
}, },
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
"version": "v3.2.6", "version": "v3.2.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/yaml.git", "url": "https://github.com/symfony/yaml.git",
"reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a" "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/093e416ad096355149e265ea2e4cc1f9ee40ab1a", "url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6",
"reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a", "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5528,20 +5591,20 @@
], ],
"description": "Symfony Yaml Component", "description": "Symfony Yaml Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-03-07T16:47:02+00:00" "time": "2017-05-01T14:55:58+00:00"
}, },
{ {
"name": "theseer/fdomdocument", "name": "theseer/fdomdocument",
"version": "1.6.1", "version": "1.6.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/theseer/fDOMDocument.git", "url": "https://github.com/theseer/fDOMDocument.git",
"reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684" "reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/8dcfd392135a5bd938c3c83ea71419501ad9855d",
"reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", "reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5568,7 +5631,7 @@
], ],
"description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
"homepage": "https://github.com/theseer/fDOMDocument", "homepage": "https://github.com/theseer/fDOMDocument",
"time": "2015-05-27T22:58:02+00:00" "time": "2017-04-21T14:50:31+00:00"
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",

View file

@ -46,8 +46,9 @@ return [
'database' => env('DB_DATABASE', 'forge'), 'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'), 'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''), 'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8', 'unix_socket' => env('DB_SOCKET', ''),
'collation' => 'utf8_unicode_ci', 'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '', 'prefix' => '',
'strict' => true, 'strict' => true,
'engine' => null, 'engine' => null,

View file

@ -14,7 +14,7 @@ return [
*/ */
'paths' => [ 'paths' => [
realpath(base_path('resources/views')), resource_path('views'),
], ],
/* /*

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateIndieWebUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('indie_web_users', function (Blueprint $table) {
$table->increments('id');
$table->string('me')->unique();
$table->text('token')->nullable();
$table->string('syntax')->default('json');
$table->jsonb('syndication')->nullable();
$table->string('mediaEndpoint')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('indie_web_users');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateNotesTableAddSwarmUrl extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('notes', function (Blueprint $table) {
$table->string('swarm_url')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('notes', function (Blueprint $table) {
$table->dropColumn('swarm_url');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdatePlacesTableAddFoursquareColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('places', function (Blueprint $table) {
$table->string('foursquare')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('places', function (Blueprint $table) {
$table->dropColumn('foursquare');
});
}
}

View file

@ -17,5 +17,6 @@ class DatabaseSeeder extends Seeder
$this->call(PlacesTableSeeder::class); $this->call(PlacesTableSeeder::class);
$this->call(NotesTableSeeder::class); $this->call(NotesTableSeeder::class);
$this->call(WebMentionsTableSeeder::class); $this->call(WebMentionsTableSeeder::class);
$this->call(IndieWebUserTableSeeder::class);
} }
} }

View file

@ -0,0 +1,16 @@
<?php
use Illuminate\Database\Seeder;
class IndieWebUserTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
App\IndieWebUser::create(['me' => config('app.url')]);
}
}

View file

@ -3,7 +3,8 @@
echo "Putting the Laravel app in maintenance mode" echo "Putting the Laravel app in maintenance mode"
php artisan down php artisan down
echo "Updating composer dependencies" echo "Updating composer and dependencies"
composer self-update
composer install composer install
echo "Caching Laravel route and config files" echo "Caching Laravel route and config files"

199
helpers.php Normal file
View file

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
/*
helpers.php
*/
// sourced from https://github.com/flattr/normalize-url/blob/master/normalize_url.php
if (! function_exists('normalize_url')) {
function normalize_url(?string $url): ?string
{
if ($url === null) {
return null;
}
$newUrl = '';
$url = parse_url($url);
$defaultSchemes = ['http' => 80, 'https' => 443];
if (isset($url['scheme'])) {
$url['scheme'] = strtolower($url['scheme']);
// Strip scheme default ports
if (isset($defaultSchemes[$url['scheme']]) &&
isset($url['port']) &&
$defaultSchemes[$url['scheme']] == $url['port']
) {
unset($url['port']);
}
$newUrl .= "{$url['scheme']}://";
}
if (isset($url['host'])) {
$url['host'] = mb_strtolower($url['host']);
$newUrl .= $url['host'];
}
if (isset($url['port'])) {
$newUrl .= ":{$url['port']}";
}
if (isset($url['path'])) {
// Case normalization
$url['path'] = normalizer_normalize($url['path'], Normalizer::FORM_C);
// Strip duplicate slashes
while (preg_match("/\/\//", $url['path'])) {
$url['path'] = preg_replace('/\/\//', '/', $url['path']);
}
/*
* Decode unreserved characters, http://www.apps.ietf.org/rfc/rfc3986.html#sec-2.3
* Heavily rewritten version of urlDecodeUnreservedChars() in Glen Scott's url-normalizer.
*/
$u = [];
for ($o = 65; $o <= 90; $o++) {
$u[] = dechex($o);
}
for ($o = 97; $o <= 122; $o++) {
$u[] = dechex($o);
}
for ($o = 48; $o <= 57; $o++) {
$u[] = dechex($o);
}
$chrs = ['-', '.', '_', '~'];
foreach ($chrs as $chr) {
$u[] = dechex(ord($chr));
}
$url['path'] = preg_replace_callback(
array_map(
create_function('$str', 'return "/%" . strtoupper($str) . "/x";'),
$u
),
create_function('$matches', 'return chr(hexdec($matches[0]));'),
$url['path']
);
// Remove directory index
$defaultIndexes = ["/default\.aspx/" => 'default.aspx/', "/default\.asp/" => 'default.asp/',
"/index\.html/" => 'index.html/', "/index\.htm/" => 'index.htm/',
"/default\.html/" => 'default.html/', "/default\.htm/" => 'default.htm/',
"/index\.php/" => 'index.php/', "/index\.jsp/" => 'index.jsp/', ];
foreach ($defaultIndexes as $index => $strip) {
if (preg_match($index, $url['path'])) {
$url['path'] = str_replace($strip, '', $url['path']);
}
}
// here we only want to drop a slash for the root domain
// e.g. http://example.com/ -> http://example.com
// but http://example.com/path/ -/-> http://example.com/path
if ($url['path'] == '/') {
unset($url['path']);
}
/**
* Path segment normalization, http://www.apps.ietf.org/rfc/rfc3986.html#sec-5.2.4
* Heavily rewritten version of removeDotSegments() in Glen Scott's url-normalizer.
*/
$new_path = '';
while (! empty($url['path'])) {
if (preg_match('!^(\.\./|\./)!x', $url['path'])) {
$url['path'] = preg_replace('!^(\.\./|\./)!x', '', $url['path']);
} elseif (preg_match('!^(/\./)!x', $url['path'], $matches)
|| preg_match('!^(/\.)$!x', $url['path'], $matches)) {
$url['path'] = preg_replace('!^' . $matches[1] . '!', '/', $url['path']);
} elseif (preg_match('!^(/\.\./|/\.\.)!x', $url['path'], $matches)) {
$url['path'] = preg_replace('!^' . preg_quote($matches[1], '!') . '!x', '/', $url['path']);
$new_path = preg_replace('!/([^/]+)$!x', '', $new_path);
} elseif (preg_match('!^(\.|\.\.)$!x', $url['path'])) {
$url['path'] = preg_replace('!^(\.|\.\.)$!x', '', $url['path']);
} else {
if (preg_match('!(/*[^/]*)!x', $url['path'], $matches)) {
$first_path_segment = $matches[1];
$url['path'] = preg_replace('/^' . preg_quote($first_path_segment, '/') . '/', '', $url['path'], 1);
$new_path .= $first_path_segment;
}
}
}
$newUrl .= $new_path;
}
if (isset($url['fragment'])) {
unset($url['fragment']);
}
// Sort GET params alphabetically, not because the RFC requires it but because it's cool!
if (isset($url['query'])) {
if (preg_match('/&/', $url['query'])) {
$s = explode('&', $url['query']);
$url['query'] = '';
sort($s);
foreach ($s as $z) {
$url['query'] .= "{$z}&";
}
$url['query'] = preg_replace('/&\Z/', '', $url['query']);
}
$newUrl .= "?{$url['query']}";
}
return $newUrl;
}
}
// sourced from https://stackoverflow.com/a/9776726
if (! function_exists('prettyPrintJson')) {
function prettyPrintJson(string $json): string
{
$result = '';
$level = 0;
$in_quotes = false;
$in_escape = false;
$ends_line_level = null;
$json_length = strlen($json);
for ($i = 0; $i < $json_length; $i++) {
$char = $json[$i];
$new_line_level = null;
$post = '';
if ($ends_line_level !== null) {
$new_line_level = $ends_line_level;
$ends_line_level = null;
}
if ($in_escape) {
$in_escape = false;
} elseif ($char === '"') {
$in_quotes = ! $in_quotes;
} elseif (! $in_quotes) {
switch ($char) {
case '}':
case ']':
$level--;
$ends_line_level = null;
$new_line_level = $level;
break;
case '{':
case '[':
$level++;
//no break
case ',':
$ends_line_level = $level;
break;
case ':':
$post = ' ';
break;
case ' ':
case "\t":
case "\n":
case "\r":
$char = '';
$ends_line_level = $new_line_level;
$new_line_level = null;
break;
}
} elseif ($char === '\\') {
$in_escape = true;
}
if ($new_line_level !== null) {
$result .= "\n".str_repeat("\t", $new_line_level);
}
$result .= $char.$post;
}
return str_replace("\t", ' ', $result);
}
}

View file

@ -6,15 +6,15 @@
"license": "CC0-1.0", "license": "CC0-1.0",
"dependencies": { "dependencies": {
"alertify.js": "^1.0.12", "alertify.js": "^1.0.12",
"mapbox-gl": "^0.34.0", "mapbox-gl": "0.37.0",
"marked": "^0.3.6", "marked": "^0.3.6",
"normalize.css": "^5.0.0", "normalize.css": "7.0.0",
"webStorage": "^1.2.2" "webStorage": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.18.0", "babel-cli": "^6.18.0",
"babel-core": "^6.21.0", "babel-core": "^6.21.0",
"babel-loader": "^6.2.10", "babel-loader": "7.0.0",
"babel-preset-env": "^1.2.2", "babel-preset-env": "^1.2.2",
"babel-preset-es2015": "^6.18.0", "babel-preset-es2015": "^6.18.0",
"babel-preset-latest": "^6.16.0", "babel-preset-latest": "^6.16.0",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,20 +1,21 @@
/* http://prismjs.com/download.html?themes=prism-dark&languages=markup+css+clike+javascript+git+http+markdown+php+php-extras+scss+sql&plugins=line-numbers+show-invisibles */ /* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript+bash+c+csharp+cpp+ruby+css-extras+diff+git+go+http+ini+json+latex+lua+makefile+markdown+nginx+objectivec+php+php-extras+python+rust+sass+scss+sql+swift+vim+wiki+yaml&plugins=line-numbers+autolinker */
/** /**
* prism.js Dark theme for JavaScript, CSS and HTML * okaidia theme for JavaScript, CSS and HTML
* Based on the slides of the talk /Reg(exp){2}lained/ * Loosely based on Monokai textmate theme by http://www.monokai.nl/
* @author Lea Verou * @author ocodia
*/ */
code[class*="language-"], code[class*="language-"],
pre[class*="language-"] { pre[class*="language-"] {
color: white; color: #f8f8f2;
text-shadow: 0 -.1em .2em black; background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
direction: ltr;
text-align: left; text-align: left;
white-space: pre; white-space: pre;
word-spacing: normal; word-spacing: normal;
word-break: normal; word-break: normal;
word-wrap: normal;
line-height: 1.5; line-height: 1.5;
-moz-tab-size: 4; -moz-tab-size: 4;
@ -27,45 +28,35 @@ pre[class*="language-"] {
hyphens: none; hyphens: none;
} }
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
pre[class*="language-"],
:not(pre) > code[class*="language-"] {
background: hsl(30, 20%, 25%);
}
/* Code blocks */ /* Code blocks */
pre[class*="language-"] { pre[class*="language-"] {
padding: 1em; padding: 1em;
margin: .5em 0; margin: .5em 0;
overflow: auto; overflow: auto;
border: .3em solid hsl(30, 20%, 40%); border-radius: 0.3em;
border-radius: .5em; }
box-shadow: 1px 1px .5em black inset;
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #272822;
} }
/* Inline code */ /* Inline code */
:not(pre) > code[class*="language-"] { :not(pre) > code[class*="language-"] {
padding: .15em .2em .05em; padding: .1em;
border-radius: .3em; border-radius: .3em;
border: .13em solid hsl(30, 20%, 40%); white-space: normal;
box-shadow: 1px 1px .3em -.1em black inset;
} }
.token.comment, .token.comment,
.token.prolog, .token.prolog,
.token.doctype, .token.doctype,
.token.cdata { .token.cdata {
color: hsl(30, 20%, 50%); color: slategray;
} }
.token.punctuation { .token.punctuation {
opacity: .7; color: #f8f8f2;
} }
.namespace { .namespace {
@ -74,11 +65,15 @@ pre[class*="language-"] {
.token.property, .token.property,
.token.tag, .token.tag,
.token.boolean,
.token.number,
.token.constant, .token.constant,
.token.symbol { .token.symbol,
color: hsl(350, 40%, 70%); .token.deleted {
color: #f92672;
}
.token.boolean,
.token.number {
color: #ae81ff;
} }
.token.selector, .token.selector,
@ -87,7 +82,7 @@ pre[class*="language-"] {
.token.char, .token.char,
.token.builtin, .token.builtin,
.token.inserted { .token.inserted {
color: hsl(75, 70%, 60%); color: #a6e22e;
} }
.token.operator, .token.operator,
@ -96,18 +91,22 @@ pre[class*="language-"] {
.language-css .token.string, .language-css .token.string,
.style .token.string, .style .token.string,
.token.variable { .token.variable {
color: hsl(40, 90%, 60%); color: #f8f8f2;
} }
.token.atrule, .token.atrule,
.token.attr-value, .token.attr-value,
.token.function {
color: #e6db74;
}
.token.keyword { .token.keyword {
color: hsl(350, 40%, 70%); color: #66d9ef;
} }
.token.regex, .token.regex,
.token.important { .token.important {
color: #e90; color: #fd971f;
} }
.token.important, .token.important,
@ -122,10 +121,6 @@ pre[class*="language-"] {
cursor: help; cursor: help;
} }
.token.deleted {
color: red;
}
pre.line-numbers { pre.line-numbers {
position: relative; position: relative;
padding-left: 3.8em; padding-left: 3.8em;
@ -166,23 +161,6 @@ pre.line-numbers > code {
padding-right: 0.8em; padding-right: 0.8em;
text-align: right; text-align: right;
} }
.token.tab:not(:empty):before, .token a {
.token.cr:before, color: inherit;
.token.lf:before {
color: hsl(24, 20%, 85%);
}
.token.tab:not(:empty):before {
content: '\21E5';
}
.token.cr:before {
content: '\240D';
}
.token.crlf:before {
content: '\240D\240A';
}
.token.lf:before {
content: '\240A';
} }

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View file

@ -7,10 +7,34 @@ Micropub Config «
@section('content') @section('content')
<p>The values for your micropub endpoint.</p> <p>The values for your micropub endpoint.</p>
<dl> <dl>
<dt>Me (your url)</dt><dd>{{ $data['me'] }}</dd> <dt>Me (your url)</dt><dd><code>{{ $data['me'] }}</code></dd>
<dt>Token</dt><dd>{{ $data['token'] }}</dd> <dt>Token</dt><dd><code>{{ $data['token'] }}</code></dd>
<dt>Syndication Targets</dt><dd>@if(is_array($data['syndication']))<ul>@foreach ($data['syndication'] as $syn)<li>{{ $syn['name'] }} ({{ $syn['target'] }})</li>@endforeach</ul>@else{{ $data['syndication'] }}@endif</dd> <dt>Syndication Targets</dt><dd>
<dt>Media Endpoint</dt><dd>{{ $data['media-endpoint'] }}</dd> @if(is_array($data['syndication']))<ul>@foreach ($data['syndication'] as $syn)
<li>{{ $syn['name'] }} ({{ $syn['target'] }})</li>
@endforeach</ul>@elseif($data['syndication'] == 'none defined')
<code>none defined</code>
@else
<pre><code class="language-json">{{ prettyPrintJson($data['syndication']) }}</code></pre>
@endif</dd>
<dt>Media Endpoint</dt><dd><code>{{ $data['media-endpoint'] }}</code></dd>
</dl> </dl>
<p>Get a <a href="{{ route('micropub-client-get-new-token') }}">new token</a>.</p>
<p><a href="{{ route('micropub-query-action') }}">Re-query</a> the endpoint.</p> <p><a href="{{ route('micropub-query-action') }}">Re-query</a> the endpoint.</p>
<p>Return to <a href="{{ route('micropub-client') }}">client</a>.
<form action="{{ route('micropub-update-syntax') }}" method="post">
{{ csrf_field() }}
<fieldset>
<legend>Syntax</legend>
<p><input type="radio" name="syntax" value="html" id="mf2"@if($data['syntax'] == 'html') checked @endif> <label for="html"><code>x-www-form-urlencoded</code> or <code>multipart/form-data</code></label></p>
<p><input type="radio" name="syntax" value="json" id="json"@if($data['syntax'] == 'json') checked @endif> <label for="json"><code>json</code></label></p>
<p><button type="submit">Update syntax</button></p>
</fieldset>
</form>
@stop
@section('scripts')
<script src="/assets/prism/prism.js"></script>
<link rel="stylesheet" href="/assets/prism/prism.css">
@stop @stop

View file

@ -89,36 +89,37 @@ Route::group(['domain' => config('url.longurl')], function () {
Route::get('blog/{year?}/{month?}', 'ArticlesController@index'); Route::get('blog/{year?}/{month?}', 'ArticlesController@index');
Route::get('blog/{year}/{month}/{slug}', 'ArticlesController@show'); Route::get('blog/{year}/{month}/{slug}', 'ArticlesController@show');
//micropub new notes page
//this needs to be first so `notes/new` doesn't match `notes/{id}`
//Notes pages using NotesController //Notes pages using NotesController
Route::get('notes', 'NotesController@index'); Route::get('notes', 'NotesController@index');
Route::get('notes/{id}', 'NotesController@show'); Route::get('notes/{id}', 'NotesController@show');
Route::get('note/{id}', 'NotesController@redirect'); Route::get('note/{id}', 'NotesController@redirect');
Route::get('notes/tagged/{tag}', 'NotesController@tagged'); Route::get('notes/tagged/{tag}', 'NotesController@tagged');
//indieauth // IndieAuth
Route::post('indieauth/start', 'IndieAuthController@start')->name('indieauth-start'); Route::post('indieauth/start', 'IndieAuthController@start')->name('indieauth-start');
Route::get('indieauth/callback', 'IndieAuthController@callback')->name('indieauth-callback'); Route::get('indieauth/callback', 'IndieAuthController@callback')->name('indieauth-callback');
Route::get('logout', 'IndieAuthController@logout')->name('indieauth-logout'); Route::get('logout', 'IndieAuthController@logout')->name('indieauth-logout');
Route::post('api/token', 'IndieAuthController@tokenEndpoint'); //hmmm?
// Token Endpoint
Route::post('api/token', 'TokenEndpointController@create');
// Micropub Client // Micropub Client
Route::get('micropub/create', 'MicropubClientController@create')->name('micropub-client'); Route::get('micropub/create', 'MicropubClientController@create')->name('micropub-client');
Route::post('micropub', 'MicropubClientController@store')->name('micropub-client-post'); Route::post('micropub', 'MicropubClientController@store')->name('micropub-client-post');
Route::get('micropub/config', 'MicropubClientController@config')->name('micropub-config'); Route::get('micropub/config', 'MicropubClientController@config')->name('micropub-config');
Route::get('micropub/get-new-token', 'MicropubClientController@getNewToken')->name('micropub-client-get-new-token');
Route::get('micropub/get-new-token/callback', 'MicropubClientController@getNewTokenCallback')->name('micropub-client-get-new-token-callback');
Route::get('micropub/query-endpoint', 'MicropubClientController@queryEndpoint')->name('micropub-query-action'); Route::get('micropub/query-endpoint', 'MicropubClientController@queryEndpoint')->name('micropub-query-action');
Route::post('micropub/update-syntax', 'MicropubClientController@updateSyntax')->name('micropub-update-syntax');
Route::get('micropub/places', 'MicropubClientController@nearbyPlaces'); Route::get('micropub/places', 'MicropubClientController@nearbyPlaces');
Route::post('micropub/places', 'MicropubClientController@newPlace'); Route::post('micropub/places', 'MicropubClientController@newPlace');
Route::post('micropub/media', 'MicropubClientController@processMedia')->name('process-media'); Route::post('micropub/media', 'MicropubClientController@processMedia')->name('process-media');
Route::get('micropub/media/clearlinks', 'MicropubClientController@clearLinks'); Route::get('micropub/media/clearlinks', 'MicropubClientController@clearLinks');
// Micropub Endpoint // Micropub Endpoints
Route::get('api/post', 'MicropubController@get'); Route::get('api/post', 'MicropubController@get')->middleware('micropub.token');
Route::post('api/post', 'MicropubController@post'); Route::post('api/post', 'MicropubController@post')->middleware('micropub.token');
Route::post('api/media', 'MicropubController@media')->name('media-endpoint'); Route::post('api/media', 'MicropubController@media')->middleware('micropub.token')->name('media-endpoint');
//webmention //webmention
Route::get('webmention', 'WebMentionsController@get'); Route::get('webmention', 'WebMentionsController@get');

2
storage/framework/testing/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -22,13 +22,16 @@ class MicropubClientTest extends DuskTestCase
public function test_client_page_creates_new_note() public function test_client_page_creates_new_note()
{ {
\Artisan::call('token:generate');
$faker = \Faker\Factory::create(); $faker = \Faker\Factory::create();
$note = 'Fake note from #LaravelDusk: ' . $faker->text; $note = 'Fake note from #LaravelDusk: ' . $faker->text;
$this->browse(function ($browser) use ($note) { $this->browse(function ($browser) use ($note) {
$browser->visit(route('micropub-client')) $browser->visit(route('micropub-client'))
->assertSeeLink('log out')
->type('content', $note) ->type('content', $note)
->press('Submit'); ->press('Submit');
}); });
sleep(2);
$this->assertDatabaseHas('notes', ['note' => $note]); $this->assertDatabaseHas('notes', ['note' => $note]);
$newNote = \App\Note::where('note', $note)->first(); $newNote = \App\Note::where('note', $note)->first();
$newNote->forceDelete(); $newNote->forceDelete();

View file

@ -19,11 +19,11 @@ class MicropubControllerTest extends TestCase
* *
* @return void * @return void
*/ */
public function test_micropub_request_without_token_returns_400_response() public function test_micropub_request_without_token_returns_401_response()
{ {
$response = $this->get('/api/post'); $response = $this->get('/api/post');
$response->assertStatus(400); $response->assertStatus(401);
$response->assertJsonFragment(['error_description' => 'No token provided with request']); $response->assertJsonFragment(['error_description' => 'No access token was provided in the request']);
} }
/** /**
@ -202,9 +202,9 @@ class MicropubControllerTest extends TestCase
$response $response
->assertJson([ ->assertJson([
'response' => 'error', 'response' => 'error',
'error' => 'no_token' 'error' => 'unauthorized'
]) ])
->assertStatus(400); ->assertStatus(401);
} }
/** /**
@ -231,9 +231,9 @@ class MicropubControllerTest extends TestCase
$response $response
->assertJson([ ->assertJson([
'response' => 'error', 'response' => 'error',
'error' => 'invalid_token' 'error' => 'insufficient_scope'
]) ])
->assertStatus(400); ->assertStatus(401);
} }
public function test_micropub_request_with_json_syntax_creates_new_place() public function test_micropub_request_with_json_syntax_creates_new_place()
@ -276,6 +276,47 @@ class MicropubControllerTest extends TestCase
->assertStatus(201); ->assertStatus(201);
} }
public function test_micropub_request_with_json_syntax_update_replace_post()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/notes/A',
'replace' => [
'content' => ['replaced content'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'updated'])
->assertStatus(200);
}
public function test_micropub_request_with_json_syntax_update_add_post()
{
$response = $this->json(
'POST',
'/api/post',
[
'action' => 'update',
'url' => config('app.url') . '/notes/A',
'add' => [
'syndication' => ['https://www.swarmapp.com/checkin/123'],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertJson(['response' => 'updated'])
->assertStatus(200);
$this->assertDatabaseHas('notes', [
'swarm_url' => 'https://www.swarmapp.com/checkin/123'
]);
}
/** /**
* Generate a valid token to be used in the tests. * Generate a valid token to be used in the tests.
* *
@ -287,7 +328,7 @@ class MicropubControllerTest extends TestCase
$token = (new Builder()) $token = (new Builder())
->set('client_id', 'https://quill.p3k.io') ->set('client_id', 'https://quill.p3k.io')
->set('me', 'https://jonnybarnes.localhost') ->set('me', 'https://jonnybarnes.localhost')
->set('scope', 'post') ->set('scope', 'create update')
->set('issued_at', time()) ->set('issued_at', time())
->sign($signer, env('APP_KEY')) ->sign($signer, env('APP_KEY'))
->getToken(); ->getToken();

View file

@ -0,0 +1,69 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class SwarmTest extends TestCase
{
use DatabaseTransactions;
public function test_faked_ownyourswarm_request()
{
$response = $this->json(
'POST',
'api/post',
[
'type' => ['h-entry'],
'properties' => [
'published' => [\Carbon\Carbon::now()->toDateTimeString()],
'syndication' => ['https://www.swarmapp.com/checkin/abc'],
'content' => [[
'value' => 'My first #checkin using Example Product',
'html' => 'My first #checkin using <a href="http://example.org">Example Product</a>',
]],
'checkin' => [[
'type' => ['h-card'],
'properties' => [
'name' => ['Awesome Venue'],
'url' => ['https://foursquare.com/v/123456'],
'latitude' => ['1.23'],
'longitude' => ['4.56'],
],
]],
],
],
['HTTP_Authorization' => 'Bearer ' . $this->getToken()]
);
$response
->assertStatus(201)
->assertJson(['response' => 'created']);
$this->assertDatabaseHas('places', [
'foursquare' => 'https://foursquare.com/v/123456'
]);
}
/**
* Generate a valid token to be used in the tests.
*
* @return Lcobucci\JWT\Token\Plain $token
*/
private function getToken()
{
$signer = new Sha256();
$token = (new Builder())
->set('client_id', 'https://ownyourswarm.p3k.io')
->set('me', 'https://jonnybarnes.localhost')
->set('scope', 'create update')
->set('issued_at', time())
->sign($signer, env('APP_KEY'))
->getToken();
return $token;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Tests\Feature;
use Mockery;
use Tests\TestCase;
use IndieAuth\Client;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class TokenEndpointTest extends TestCase
{
public function test_token_endpoint_issues_token()
{
$mockClient = Mockery::mock(Client::class);
$mockClient->shouldReceive('discoverAuthorizationEndpoint')
->with(normalize_url(config('app.url')))
->once()
->andReturn('https://indieauth.com/auth');
$mockClient->shouldReceive('verifyIndieAuthCode')
->andReturn([
'me' => config('app.url'),
'scope' => 'create update',
]);
$this->app->instance(Client::class, $mockClient);
$response = $this->post('/api/token', [
'me' => config('app.url'),
'code' => 'abc123',
'redirect_uri' => route('indieauth-callback'),
'client_id' => route('micropub-client'),
'state' => mt_rand(1000, 10000),
]);
parse_str($response->content(), $output);
$this->assertEquals(config('app.url'), $output['me']);
$this->assertTrue(array_key_exists('access_token', $output));
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class HelpersTest extends TestCase
{
public function test_normalize_url_is_idempotent()
{
$input = 'http://example.org:80/index.php?foo=bar&baz=1';
$this->assertEquals(normalize_url(normalize_url($input)), normalize_url($input));
}
/**
* @dataProvider urlProvider
*/
public function test_normalize_url($input, $output)
{
$this->assertEquals($output, normalize_url($input));
}
public function urlProvider()
{
return [
['https://example.org/', 'https://example.org'],
['https://example.org:443/', 'https://example.org'],
['http://www.foo.bar/index.php/', 'http://www.foo.bar'],
['https://example.org/?foo=bar&baz=true', 'https://example.org?baz=true&foo=bar'],
];
}
public function test_pretty_print_json()
{
$json = <<<JSON
{"glossary": {"title": "example glossary", "GlossDiv": {"title": "S", "GlossList": {"GlossEntry": {"ID": "SGML", "SortAs": "SGML", "GlossTerm": "Standard Generalized Markup Language", "Acronym": "SGML", "Abbrev": "ISO 8879:1986", "GlossDef": {"para": "A meta-markup language, used to create markup languages such as DocBook.", "GlossSeeAlso": ["GML", "XML"]}, "GlossSee": "markup"}}}}}
JSON;
$expected = <<<EXPECTED
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": [
"GML",
"XML"
]
},
"GlossSee": "markup"
}
}
}
}
}
EXPECTED;
$this->assertEquals($expected, prettyPrintJson($json));
}
}

38
tinker.config.php Normal file
View file

@ -0,0 +1,38 @@
<?php
/**
* Automatically alias Laravel Model's to their base classname.
* Ex: "App\Models\User" now can just be accessed by "User"
*/
if (! function_exists('aliasModels')) {
function aliasModels() {
$finder = new \Symfony\Component\Finder\Finder();
$finder->files()->name('*.php')->in(base_path().'/app');
foreach ($finder as $file) {
$namespace = 'App\\';
if ($relativePath = $file->getRelativePath()) {
$namespace .= strtr($relativePath, '/', '\\') . '\\';
}
$class = $namespace . $file->getBasename('.php');
try {
$r = new \ReflectionClass($class);
if ($r->isSubclassOf('Illuminate\\Database\\Eloquent\\Model')) {
class_alias($class, $file->getBasename('.php'));
}
} catch (Exception $e) {
//
}
}
}
}
aliasModels();
return [
'startupMessage' => '<info>Using local config file (tinker.config.php)</info>',
'commands' => [
// new \App\Tinker\TestCommand,
],
];

View file

@ -1,4 +1,5 @@
[global] [global]
error_log = /tmp/php-fpm.error.log
[travis] [travis]
user = {USER} user = {USER}

885
yarn.lock

File diff suppressed because it is too large Load diff