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
MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
@ -52,3 +52,5 @@ TWITTER_ACCESS_TOKEN_SECRET=
SCOUT_DRIVER=pgsql
PIWIK=false
PSYSH_CONFIG=tinker.config.php

1
.gitattributes vendored
View file

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

View file

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

View file

@ -2,9 +2,9 @@
namespace App\Console\Commands;
use App\IndieWebUser;
use App\Services\TokenService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class GenerateToken extends Command
{
@ -49,10 +49,12 @@ class GenerateToken extends Command
$data = [
'me' => config('app.url'),
'client_id' => route('micropub-client'),
'scope' => 'post',
'scope' => 'create update',
];
$token = $tokenService->getNewToken($data);
Storage::disk('local')->put('dev-token', $token);
$user = IndieWebUser::where('me', config('app.url'))->first();
$user->token = $token;
$user->save();
$this->info('Set token');
}

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

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Services\IndieAuthService;
use App\IndieWebUser;
use IndieAuth\Client as IndieClient;
use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Http\{Request, Response};
@ -10,20 +10,13 @@ use GuzzleHttp\Exception\{ClientException, ServerException};
class MicropubClientController extends Controller
{
/**
* The IndieAuth service container.
*/
protected $indieAuthService;
/**
* Inject the dependencies.
*/
public function __construct(
IndieAuthService $indieAuthService = null,
IndieClient $indieClient = null,
GuzzleClient $guzzleClient = null
) {
$this->indieAuthService = $indieAuthService ?? new IndieAuthService();
$this->guzzleClient = $guzzleClient ?? new GuzzleClient();
$this->indieClient = $indieClient ?? new IndieClient();
}
@ -37,8 +30,11 @@ class MicropubClientController extends Controller
public function create(Request $request)
{
$url = $request->session()->get('me');
$syndication = $request->session()->get('syndication');
$mediaEndpoint = $request->session()->get('media-endpoint');
if ($url) {
$indiewebUser = IndieWebUser::where('me', $url)->first();
}
$syndication = $this->parseSyndicationTargets($indiewebUser->syndication);
$mediaEndpoint = $indiewebUser->mediaEndpoint ?? null;
$mediaURLs = $request->session()->get('media-links');
return view('micropub.create', compact('url', 'syndication', 'mediaEndpoint', 'mediaURLs'));
@ -56,19 +52,17 @@ class MicropubClientController extends Controller
return back();
}
$mediaEndpoint = $request->session()->get('media-endpoint');
if ($mediaEndpoint == null) {
$user = IndieWebUser::where('me', $request->session()->get('me'))->firstOrFail();
if ($user->mediaEndpoint == null || $user->token == null) {
return back();
}
$token = $request->session()->get('token');
$mediaURLs = [];
foreach ($request->file('file') as $file) {
try {
$response = $this->guzzleClient->request('POST', $mediaEndpoint, [
$response = $this->guzzleClient->request('POST', $user->mediaEndpoint, [
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Authorization' => 'Bearer ' . $user->token,
],
'multipart' => [
[
@ -109,27 +103,142 @@ class MicropubClientController extends Controller
*/
public function store(Request $request)
{
$domain = $request->session()->get('me');
$token = $request->session()->get('token');
$url = normalize_url($request->session()->get('me'));
$user = IndieWebUser::where('me', $url)->firstOrFail();
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint(
$domain,
$this->indieClient
);
if ($user->token == null) {
return redirect(route('micropub-client'))->with('error', 'You havent requested a token yet');
}
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
if (! $micropubEndpoint) {
return redirect(route('micropub-client'))->with('error', 'Unable to determine micropub API endpoint');
}
$response = $this->postNoteRequest($request, $micropubEndpoint, $token);
$headers = [
'Authorization' => 'Bearer ' . $user->token,
];
if ($response->getStatusCode() == 201) {
$request->session()->forget('media-links');
$location = $response->getHeader('Location');
if (is_array($location)) {
return redirect($location[0]);
if ($user->syntax == 'html') {
$multipart = [
[
'name' => 'h',
'contents' => 'entry',
],
[
'name' => 'content',
'contents' => $request->input('content'),
],
];
if ($request->hasFile('photo')) {
$photos = $request->file('photo');
foreach ($photos as $photo) {
$multipart[] = [
'name' => 'photo[]',
'contents' => fopen($photo->path(), 'r'),
'filename' => $photo->getClientOriginalName(),
];
}
}
if ($request->input('in-reply-to') != '') {
$multipart[] = [
'name' => 'in-reply-to',
'contents' => $request->input('in-reply-to'),
];
}
if ($request->input('mp-syndicate-to')) {
foreach ($request->input('mp-syndicate-to') as $syn) {
$multipart[] = [
'name' => 'mp-syndicate-to[]',
'contents' => $syn,
];
}
}
if ($request->input('location')) {
if ($request->input('location') !== 'no-location') {
$multipart[] = [
'name' => 'location',
'contents' => $request->input('location'),
];
}
}
if ($request->input('media')) {
foreach ($request->input('media') as $media) {
$multipart[] = [
'name' => 'photo[]',
'contents' => $media,
];
}
}
try {
$response = $this->guzzleClient->post($micropubEndpoint, [
'multipart' => $multipart,
'headers' => $headers,
]);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
return redirect(route('micropub-client'))->with(
'error',
'There was a bad response from the micropub endpoint.'
);
}
return redirect($location);
if ($response->getStatusCode() == 201) {
$request->session()->forget('media-links');
$location = $response->getHeader('Location');
if (is_array($location)) {
return redirect($location[0]);
}
return redirect($location);
}
}
if ($user->syntax == 'json') {
$json = [];
$json['type'] = ['h-entry'];
$json['properties'] = ['content' => [$request->input('content')]];
if ($request->input('in-reply-to') != '') {
$json['properties']['in-reply-to'] = [$request->input('in-reply-to')];
}
if ($request->input('mp-syndicate-to')) {
foreach ($request->input('mp-syndicate-to') as $syn) {
$json['properties']['mp-syndicate-to'] = [$syn];
}
}
if ($request->input('location')) {
if ($request->input('location') !== 'no-location') {
$json['properties']['location'] = [$request->input('location')];
}
}
if ($request->input('media')) {
$json['properties']['photo'] = [];
foreach ($request->input('media') as $media) {
$json['properties']['photo'][] = $media;
}
}
try {
$response = $this->guzzleClient->post($micropubEndpoint, [
'json' => $json,
'headers' => $headers,
]);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
return redirect(route('micropub-client'))->with(
'error',
'There was a bad response from the micropub endpoint.'
);
}
if ($response->getStatusCode() == 201) {
$request->session()->forget('media-links');
$location = $response->getHeader('Location');
if (is_array($location)) {
return redirect($location[0]);
}
return redirect($location);
}
}
return redirect(route('micropub-client'))->with('error', 'Endpoint didnt create the note.');
@ -143,26 +252,100 @@ class MicropubClientController extends Controller
*/
public function config(Request $request)
{
$data['me'] = $request->session()->get('me');
$data['token'] = $request->session()->get('token');
$data['syndication'] = $request->session()->get('syndication') ?? 'none defined';
$data['media-endpoint'] = $request->session()->get('media-endpoint') ?? 'none defined';
//default values
$data = [
'me' => '',
'token' => 'none',
'syndication' => 'none defined',
'media-endpoint' => 'none defined',
'syntax' => 'html',
];
if ($request->session()->has('me')) {
$data['me'] = normalize_url($request->session()->get('me'));
$user = IndieWebUser::where('me', $request->session()->get('me'))->first();
$data['token'] = $user->token ?? 'none defined';
$data['syndication'] = $user->syndication ?? 'none defined';
$data['media-endpoint'] = $user->mediaEndpoint ?? 'none defined';
$data['syntax'] = $user->syntax;
}
return view('micropub.config', compact('data'));
}
/**
* Query the micropub endpoint and store response in the session.
* Get a new token.
*
* @param Illuminate\Http\Request $request
* @return view
*/
public function getNewToken(Request $request)
{
if ($request->session()->has('me')) {
$url = normalize_url($request->session()->get('me'));
$authozationEndpoint = $this->indieClient->discoverAuthorizationEndpoint($url);
if ($authozationEndpoint) {
$state = bin2hex(random_bytes(16));
$request->session()->put('state', $state);
$authorizationURL = $this->indieClient->buildAuthorizationURL(
$authozationEndpoint,
$url,
route('micropub-client-get-new-token-callback'), // redirect_uri
route('micropub-client'), //client_id
$state,
'create update' // scope needs to be a setting
);
return redirect($authorizationURL);
}
return back();
}
return back();
}
/**
* The callback for getting a token.
*/
public function getNewTokenCallback(Request $request)
{
if ($request->input('state') !== $request->session()->get('state')) {
return route('micropub-client')->with('error', 'The <code>state</code> 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)
{
$domain = $request->session()->get('me');
$token = $request->session()->get('token');
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint($domain);
if ($micropubEndpoint !== null) {
$url = normalize_url($request->session()->get('me'));
$user = IndieWebUser::where('me', $url)->firstOrFail();
$token = $user->token;
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
if ($micropubEndpoint) {
try {
$response = $this->guzzleClient->get($micropubEndpoint, [
'headers' => ['Authorization' => 'Bearer ' . $token],
@ -172,96 +355,35 @@ class MicropubClientController extends Controller
return back();
}
$body = (string) $response->getBody();
$data = json_decode($body, true);
$syndication = $this->parseSyndicationTargets($body);
$request->session()->put('syndication', $syndication);
if (array_key_exists('syndicate-to', $data)) {
$user->syndication = json_encode($data['syndicate-to']);
}
$mediaEndpoint = $this->parseMediaEndpoint($body);
$request->session()->put('media-endpoint', $mediaEndpoint);
if (array_key_exists('media-endpoint', $data)) {
$user->mediaEndpoint = $data['media-endpoint'];
}
$user->save();
return back();
}
}
/**
* This method performs the actual POST request.
* Update the syntax setting.
*
* @param \Illuminate\Http\Request $request
* @param string The Micropub endpoint to post to
* @param string The token to authenticate the request with
* @return \GuzzleHttp\Response $response | \Illuminate\RedirectFactory redirect
* @param Illuminate\Http\Request $request
* @return Illuminate\Http\RedirectResponse
* @todo validate input
*/
private function postNoteRequest(
Request $request,
$micropubEndpoint,
$token
) {
$multipart = [
[
'name' => 'h',
'contents' => 'entry',
],
[
'name' => 'content',
'contents' => $request->input('content'),
],
];
if ($request->hasFile('photo')) {
$photos = $request->file('photo');
foreach ($photos as $photo) {
$multipart[] = [
'name' => 'photo[]',
'contents' => fopen($photo->path(), 'r'),
'filename' => $photo->getClientOriginalName(),
];
}
}
if ($request->input('in-reply-to') != '') {
$multipart[] = [
'name' => 'in-reply-to',
'contents' => $request->input('in-reply-to'),
];
}
if ($request->input('mp-syndicate-to')) {
foreach ($request->input('mp-syndicate-to') as $syn) {
$multipart[] = [
'name' => 'mp-syndicate-to[]',
'contents' => $syn,
];
}
}
if ($request->input('location')) {
if ($request->input('location') !== 'no-location') {
$multipart[] = [
'name' => 'location',
'contents' => $request->input('location'),
];
}
}
if ($request->input('media')) {
foreach ($request->input('media') as $media) {
$multipart[] = [
'name' => 'photo[]',
'contents' => $media,
];
}
}
$headers = [
'Authorization' => 'Bearer ' . $token,
];
try {
$response = $this->guzzleClient->post($micropubEndpoint, [
'multipart' => $multipart,
'headers' => $headers,
]);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
return redirect(route('micropub-client'))->with(
'error',
'There was a bad response from the micropub endpoint.'
);
}
public function updateSyntax(Request $request)
{
$user = IndieWebUser::where('me', $request->session()->get('me'))->firstOrFail();
$user->syntax = $request->syntax;
$user->save();
return $response;
return redirect(route('micropub-config'));
}
/**
@ -272,16 +394,17 @@ class MicropubClientController extends Controller
*/
public function newPlace(Request $request)
{
if ($request->session()->has('token') === false) {
$url = normalize_url($request->session()->get('me'));
$user = IndieWebUser::where('me', $url)->firstOrFail();
if ($user->token === null) {
return response()->json([
'error' => true,
'error_description' => 'No known token',
], 400);
}
$domain = $request->session()->get('me');
$token = $request->session()->get('token');
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint($domain, $this->indieClient);
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
if (! $micropubEndpoint) {
return response()->json([
'error' => true,
@ -289,13 +412,27 @@ class MicropubClientController extends Controller
], 400);
}
$place = $this->postPlaceRequest($request, $micropubEndpoint, $token);
if ($place === false) {
$formParams = [
'h' => 'card',
'name' => $request->input('place-name'),
'description' => $request->input('place-description'),
'geo' => 'geo:' . $request->input('place-latitude') . ',' . $request->input('place-longitude'),
];
$headers = [
'Authorization' => 'Bearer ' . $user->token,
];
try {
$response = $this->guzzleClient->request('POST', $micropubEndpoint, [
'form_params' => $formParams,
'headers' => $headers,
]);
} catch (ClientException $e) {
return response()->json([
'error' => true,
'error_description' => 'Unable to create the new place',
], 400);
}
$place = $response->getHeader('Location')[0];
return response()->json([
'uri' => $place,
@ -305,44 +442,6 @@ class MicropubClientController extends Controller
]);
}
/**
* Actually make a micropub request to make a new place.
*
* @param \Illuminate\Http\Request $request
* @param string The Micropub endpoint to post to
* @param string The token to authenticate the request with
* @param \GuzzleHttp\Client $client
* @return \GuzzleHttp\Response $response | \Illuminate\RedirectFactory redirect
*/
private function postPlaceRequest(
Request $request,
$micropubEndpoint,
$token
) {
$formParams = [
'h' => 'card',
'name' => $request->input('place-name'),
'description' => $request->input('place-description'),
'geo' => 'geo:' . $request->input('place-latitude') . ',' . $request->input('place-longitude'),
];
$headers = [
'Authorization' => 'Bearer ' . $token,
];
try {
$response = $this->guzzleClient->request('POST', $micropubEndpoint, [
'form_params' => $formParams,
'headers' => $headers,
]);
} catch (ClientException $e) {
return false;
}
if ($response->getStatusCode() == 201) {
return $response->getHeader('Location')[0];
}
return false;
}
/**
* Make a request to the micropub endpoint requesting any nearby places.
*
@ -351,16 +450,17 @@ class MicropubClientController extends Controller
*/
public function nearbyPlaces(Request $request)
{
if ($request->session()->has('token') === false) {
$url = normalize_url($request->session()->get('me'));
$user = IndieWebUser::where('me', $url)->firstOrFail();
if ($user->token === null) {
return response()->json([
'error' => true,
'error_description' => 'No known token',
], 400);
}
$domain = $request->session()->get('me');
$token = $request->session()->get('token');
$micropubEndpoint = $this->indieAuthService->discoverMicropubEndpoint($domain, $this->indieClient);
$micropubEndpoint = $this->indieClient->discoverMicropubEndpoint($url);
if (! $micropubEndpoint) {
return response()->json([
@ -375,7 +475,7 @@ class MicropubClientController extends Controller
$query .= ';u=' . $request->input('u');
}
$response = $this->guzzleClient->get($micropubEndpoint, [
'headers' => ['Authorization' => 'Bearer ' . $token],
'headers' => ['Authorization' => 'Bearer ' . $user->token],
'query' => ['q' => $query],
]);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
@ -390,30 +490,34 @@ class MicropubClientController extends Controller
}
/**
* Parse the syndication targets retreived from a cookie, to a form that can
* be used in a view.
* Parse the syndication targets JSON into a an array.
*
* @param string $syndicationTargets
* @param string|null
* @return array|null
*/
private function parseSyndicationTargets($syndicationTargets = null)
{
if ($syndicationTargets === null) {
if ($syndicationTargets === null || $syndicationTargets === '') {
return;
}
$syndicateTo = [];
$data = json_decode($syndicationTargets, true);
if (array_key_exists('syndicate-to', $data)) {
foreach ($data['syndicate-to'] as $syn) {
if (array_key_exists('uid', $data)) {
$syndicateTo[] = [
'target' => $data['uid'],
'name' => $data['name'],
];
}
foreach ($data as $syn) {
if (array_key_exists('uid', $syn)) {
$syndicateTo[] = [
'target' => $syn['uid'],
'name' => $syn['name'],
];
}
}
if (count($syndicateTo) > 0) {
return $syndicateTo;
}
return $syndicateTo;
}
/**

View file

@ -3,8 +3,9 @@
namespace App\Http\Controllers;
use Ramsey\Uuid\Uuid;
use App\{Media, Place};
use App\{Media, Note, Place};
use Illuminate\Http\{Request, Response};
use App\Exceptions\InvalidTokenException;
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
use App\Services\{NoteService, PlaceService, TokenService};
@ -47,34 +48,67 @@ class MicropubController extends Controller
*/
public function post(Request $request)
{
$httpAuth = $request->header('Authorization');
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) {
$token = $match[1];
$tokenData = $this->tokenService->validateToken($token);
if ($tokenData->hasClaim('scope')) {
$scopes = explode(' ', $tokenData->getClaim('scope'));
if (array_search('post', $scopes) !== false) {
$clientId = $tokenData->getClaim('client_id');
if (($request->input('h') == 'entry') || ($request->input('type')[0] == 'h-entry')) {
$data = [];
$data['client-id'] = $clientId;
if ($request->header('Content-Type') == 'application/json') {
$data['content'] = $request->input('properties.content')[0];
$data['in-reply-to'] = $request->input('properties.in-reply-to')[0];
$data['location'] = $request->input('properties.location');
//flatten location if array
if (is_array($data['location'])) {
$data['location'] = $data['location'][0];
}
} else {
$data['content'] = $request->input('content');
$data['in-reply-to'] = $request->input('in-reply-to');
$data['location'] = $request->input('location');
try {
$tokenData = $this->tokenService->validateToken($request->bearerToken());
} catch (InvalidTokenException $e) {
return response()->json([
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
}
if ($tokenData->hasClaim('scope')) {
if (($request->input('h') == 'entry') || ($request->input('type')[0] == 'h-entry')) {
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->returnInsufficientScopeResponse();
}
$data = [];
$data['client-id'] = $tokenData->getClaim('client_id');
if ($request->header('Content-Type') == 'application/json') {
if (is_string($request->input('properties.content')[0])) {
$data['content'] = $request->input('properties.content')[0]; //plaintext content
}
if (is_array($request->input('properties.content')[0])
&& array_key_exists('html', $request->input('properties.content')[0])
) {
$data['content'] = $request->input('properties.content')[0]['html'];
}
$data['in-reply-to'] = $request->input('properties.in-reply-to')[0];
$data['location'] = $request->input('properties.location');
//flatten location if array
if (is_array($data['location'])) {
$data['location'] = $data['location'][0];
}
$data['published'] = $request->input('properties.published')[0];
//create checkin place
if (array_key_exists('checkin', $request->input('properties'))) {
$data['checkin'] = $request->input('properties.checkin.0.properties.url.0');
try {
$this->placeService->createPlaceFromCheckin($request->input('properties.checkin.0'));
} catch (\Exception $e) {
$data['checkin'] = null;
}
$data['syndicate'] = [];
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
if (is_string($request->input('mp-syndicate-to'))) {
$service = array_search($request->input('mp-syndicate-to'));
}
} else {
$data['content'] = $request->input('content');
$data['in-reply-to'] = $request->input('in-reply-to');
$data['location'] = $request->input('location');
$data['published'] = $request->input('published');
}
$data['syndicate'] = [];
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
if (is_string($request->input('mp-syndicate-to'))) {
$service = array_search($request->input('mp-syndicate-to'));
if ($service == 'Twitter') {
$data['syndicate'][] = 'twitter';
}
if ($service == 'Facebook') {
$data['syndicate'][] = 'facebook';
}
}
if (is_array($request->input('mp-syndicate-to'))) {
foreach ($targets as $service => $target) {
if (in_array($target, $request->input('mp-syndicate-to'))) {
if ($service == 'Twitter') {
$data['syndicate'][] = 'twitter';
}
@ -82,83 +116,145 @@ class MicropubController extends Controller
$data['syndicate'][] = 'facebook';
}
}
if (is_array($request->input('mp-syndicate-to'))) {
foreach ($targets as $service => $target) {
if (in_array($target, $request->input('mp-syndicate-to'))) {
if ($service == 'Twitter') {
$data['syndicate'][] = 'twitter';
}
if ($service == 'Facebook') {
$data['syndicate'][] = 'facebook';
}
}
}
}
$data['photo'] = [];
if (is_array($request->input('photo'))) {
foreach ($request->input('photo') as $photo) {
if (is_string($photo)) {
//only supporting media URLs for now
$data['photo'][] = $photo;
}
}
}
try {
$note = $this->noteService->createNote($data);
} catch (Exception $exception) {
return response()->json(['error' => true], 400);
}
return response()->json([
'response' => 'created',
'location' => $note->longurl,
], 201)->header('Location', $note->longurl);
}
if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') {
$data = [];
if ($request->header('Content-Type') == 'application/json') {
$data['name'] = $request->input('properties.name');
$data['description'] = $request->input('properties.description') ?? null;
if ($request->has('properties.geo')) {
$data['geo'] = $request->input('properties.geo');
}
$data['photo'] = [];
if (is_array($request->input('photo'))) {
foreach ($request->input('photo') as $photo) {
if (is_string($photo)) {
//only supporting media URLs for now
$data['photo'][] = $photo;
}
}
}
try {
$note = $this->noteService->createNote($data);
} catch (\Exception $exception) {
return response()->json(['error' => true], 400);
}
return response()->json([
'response' => 'created',
'location' => $note->longurl,
], 201)->header('Location', $note->longurl);
}
if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') {
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->returnInsufficientScopeResponse();
}
$data = [];
if ($request->header('Content-Type') == 'application/json') {
$data['name'] = $request->input('properties.name');
$data['description'] = $request->input('properties.description') ?? null;
if ($request->has('properties.geo')) {
$data['geo'] = $request->input('properties.geo');
}
} else {
$data['name'] = $request->input('name');
$data['description'] = $request->input('description');
if ($request->has('geo')) {
$data['geo'] = $request->input('geo');
}
if ($request->has('latitude')) {
$data['latitude'] = $request->input('latitude');
$data['longitude'] = $request->input('longitude');
}
}
try {
$place = $this->placeService->createPlace($data);
} catch (\Exception $exception) {
return response()->json(['error' => true], 400);
}
return response()->json([
'response' => 'created',
'location' => $place->longurl,
], 201)->header('Location', $place->longurl);
}
if ($request->input('action') == 'update') {
if (stristr($tokenData->getClaim('scope'), 'update') === false) {
return $this->returnInsufficientScopeResponse();
}
$urlPath = parse_url($request->input('url'), PHP_URL_PATH);
//is it a note we are updating?
if (mb_substr($urlPath, 1, 5) === 'notes') {
try {
$note = Note::nb60(basename($urlPath))->first();
} catch (\Exception $exception) {
return response()->json([
'error' => 'invalid_request',
'error_description' => 'No known note with given ID',
]);
}
//got the note, are we dealing with a “replace” request?
if ($request->has('replace')) {
foreach ($request->input('replace') as $property => $value) {
if ($property == 'content') {
$note->note = $value[0];
}
} else {
$data['name'] = $request->input('name');
$data['description'] = $request->input('description');
if ($request->has('geo')) {
$data['geo'] = $request->input('geo');
}
if ($request->has('latitude')) {
$data['latitude'] = $request->input('latitude');
$data['longitude'] = $request->input('longitude');
if ($property == 'syndication') {
foreach ($value as $syndicationURL) {
if (starts_with($syndicationURL, 'https://www.facebook.com')) {
$note->facebook_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://www.swarmapp.com')) {
$note->swarm_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://twitter.com')) {
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
}
}
}
}
try {
$place = $this->placeService->createPlace($data);
} catch (Exception $exception) {
return response()->json(['error' => true], 400);
}
$note->save();
return response()->json([
'response' => 'created',
'location' => $place->longurl,
], 201)->header('Location', $place->longurl);
'response' => 'updated',
]);
}
//how about “add”
if ($request->has('add')) {
foreach ($request->input('add') as $property => $value) {
if ($property == 'syndication') {
foreach ($value as $syndicationURL) {
if (starts_with($syndicationURL, 'https://www.facebook.com')) {
$note->facebook_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://www.swarmapp.com')) {
$note->swarm_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://twitter.com')) {
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
}
}
}
if ($property == 'photo') {
foreach ($value as $photoURL) {
if (start_with($photo, 'https://')) {
$media = new Media();
$media->path = $photoURL;
$media->type = 'image';
$media->save();
$note->media()->save($media);
}
}
}
}
$note->save();
return response()->json([
'response' => 'updated',
]);
}
}
}
return response()->json([
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The token provided is not valid or does not have the necessary scope',
], 400);
}
return response()->json([
'response' => 'error',
'error' => 'no_token',
'error_description' => 'No OAuth token sent with request',
], 400);
'error' => 'forbidden',
'error_description' => 'The token has no scopes',
], 403);
}
/**
@ -172,68 +268,58 @@ class MicropubController extends Controller
*/
public function get(Request $request)
{
$httpAuth = $request->header('Authorization');
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) {
$token = $match[1];
$valid = $this->tokenService->validateToken($token);
if ($valid === null) {
return response()->json([
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
}
//we have a valid token, is `syndicate-to` set?
if ($request->input('q') === 'syndicate-to') {
return response()->json([
'syndicate-to' => config('syndication.targets'),
]);
}
//nope, how about a config query?
if ($request->input('q') == 'config') {
return response()->json([
'syndicate-to' => config('syndication.targets'),
'media-endpoint' => route('media-endpoint'),
]);
}
//nope, how about a geo URL?
if (substr($request->input('q'), 0, 4) === 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$request->input('q'),
$matches
);
$distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000;
$places = Place::near($matches[0][0], $matches[0][1], $distance);
foreach ($places as $place) {
$place->uri = config('app.url') . '/places/' . $place->slug;
}
return response()->json([
'response' => 'places',
'places' => $places,
]);
}
//nope, just return the token
try {
$tokenData = $this->tokenService->validateToken($request->bearerToken());
} catch (InvalidTokenException $e) {
return response()->json([
'response' => 'token',
'token' => [
'me' => $valid->getClaim('me'),
'scope' => $valid->getClaim('scope'),
'client_id' => $valid->getClaim('client_id'),
],
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
}
//we have a valid token, is `syndicate-to` set?
if ($request->input('q') === 'syndicate-to') {
return response()->json([
'syndicate-to' => config('syndication.targets'),
]);
}
//nope, how about a config query?
if ($request->input('q') == 'config') {
return response()->json([
'syndicate-to' => config('syndication.targets'),
'media-endpoint' => route('media-endpoint'),
]);
}
//nope, how about a geo URL?
if (substr($request->input('q'), 0, 4) === 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$request->input('q'),
$matches
);
$distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000;
$places = Place::near($matches[0][0], $matches[0][1], $distance);
foreach ($places as $place) {
$place->uri = config('app.url') . '/places/' . $place->slug;
}
return response()->json([
'response' => 'places',
'places' => $places,
]);
}
//nope, just return the token
return response()->json([
'response' => 'error',
'error' => 'no_token',
'error_description' => 'No token provided with request',
], 400);
'response' => 'token',
'token' => [
'me' => $tokenData->getClaim('me'),
'scope' => $tokenData->getClaim('scope'),
'client_id' => $tokenData->getClaim('client_id'),
],
]);
}
/**
@ -244,82 +330,65 @@ class MicropubController extends Controller
*/
public function media(Request $request)
{
//can this go in middleware
$httpAuth = $request->header('Authorization');
if (preg_match('/Bearer (.+)/', $httpAuth, $match)) {
$token = $match[1];
$tokenData = $this->tokenService->validateToken($token);
try {
$tokenData = $this->tokenService->validateToken($request->bearerToken());
} catch (InvalidTokenException $e) {
return response()->json([
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
}
if ($tokenData === null) {
return response()->json([
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
//check post scope
if ($tokenData->hasClaim('scope')) {
if (stristr($token->getClaim('scope'), 'post') === false) {
return $this->returnInsufficientScopeResponse();
}
//check post scope
if ($tokenData->hasClaim('scope')) {
$scopes = explode(' ', $tokenData->getClaim('scope'));
if (array_search('post', $scopes) !== false) {
//check media valid
if ($request->hasFile('file') && $request->file('file')->isValid()) {
$type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType());
try {
$filename = Uuid::uuid4() . '.' . $request->file('file')->extension();
} catch (UnsatisfiedDependencyException $e) {
return response()->json([
'response' => 'error',
'error' => 'internal_server_error',
'error_description' => 'A problem occured handling your request',
], 500);
}
try {
$path = $request->file('file')->storeAs('media', $filename, 's3');
} catch (Exception $e) { // which exception?
return response()->json([
'response' => 'error',
'error' => 'service_unavailable',
'error_description' => 'Unable to save media to S3',
], 503);
}
$media = new Media();
$media->token = $token;
$media->path = $path;
$media->type = $type;
$media->save();
return response()->json([
'response' => 'created',
'location' => $media->url,
], 201)->header('Location', $media->url);
}
//check media valid
if ($request->hasFile('file') && $request->file('file')->isValid()) {
$type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType());
try {
$filename = Uuid::uuid4() . '.' . $request->file('file')->extension();
} catch (UnsatisfiedDependencyException $e) {
return response()->json([
'response' => 'error',
'error' => 'invalid_request',
'error_description' => 'The uploaded file failed validation',
], 400);
'error' => 'internal_server_error',
'error_description' => 'A problem occured handling your request',
], 500);
}
try {
$path = $request->file('file')->storeAs('media', $filename, 's3');
} catch (Exception $e) { // which exception?
return response()->json([
'response' => 'error',
'error' => 'service_unavailable',
'error_description' => 'Unable to save media to S3',
], 503);
}
$media = new Media();
$media->token = $request->bearerToken();
$media->path = $path;
$media->type = $type;
$media->save();
return response()->json([
'response' => 'error',
'error' => 'insufficient_scope',
'error_description' => 'The provided token has insufficient scopes',
], 401);
'response' => 'created',
'location' => $media->url,
], 201)->header('Location', $media->url);
}
return response()->json([
'response' => 'error',
'error' => 'unauthorized',
'error_description' => 'No token provided with request',
], 401);
'error' => 'invalid_request',
'error_description' => 'The uploaded file failed validation',
], 400);
}
return response()->json([
'response' => 'error',
'error' => 'no_token',
'error_description' => 'There was no token provided with the request',
'error' => 'invalid_request',
'error_description' => 'The provided token has no scopes',
], 400);
}
@ -366,4 +435,13 @@ class MicropubController extends Controller
return 'download';
}
private function returnInsufficientScopeResponse()
{
return response()->json([
'response' => 'error',
'error' => 'insufficient_scope',
'error_description' => 'The 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,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\LinkHeadersMiddleware::class,
\App\Http\Middleware\DevTokenMiddleware::class,
//\App\Http\Middleware\DevTokenMiddleware::class,
\App\Http\Middleware\LocalhostSessionMiddleware::class,
],
'api' => [
@ -58,5 +59,6 @@ class Kernel extends HttpKernel
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'myauth' => \App\Http\Middleware\MyAuthMiddleware::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'micropub.token' => \App\Http\Middleware\VerifyMicropubToken::class,
];
}

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()
{
if (starts_with($this->path, 'https://')) {
return $this->path;
}
return config('filesystems.disks.s3.url') . '/' . $this->path;
}
}

View file

@ -191,6 +191,20 @@ class Note extends Model
return $name;
}
/**
* Scope a query to select a note via a NewBase60 id.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $nb60id
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNb60($query, $nb60id)
{
$numbers = new Numbers();
return $query->where('id', $numbers->b60tonum($nb60id));
}
/**
* Take note that this method does two things, given @username (NOT [@username](URL)!)
* we try to create a fancy hcard from our contact info. If this is not possible

View file

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

View file

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

View file

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

View file

@ -17,6 +17,16 @@ class NoteService
*/
public function createNote(array $data): Note
{
//check the input
if (array_key_exists('content', $data) === false) {
throw new \Exception('No content defined'); //we 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' => $data['content'],
@ -25,6 +35,11 @@ class NoteService
]
);
if (array_key_exists('published', $data) && empty($data['published']) === false) {
$carbon = new \Carbon\Carbon($data['published']);
$note->created_at = $note->updated_at = $carbon->toDateTimeString();
}
if (array_key_exists('location', $data) && $data['location'] !== null && $data['location'] !== 'no-location') {
if (starts_with($data['location'], config('app.url'))) {
//uri of form http://host/places/slug, we want slug
@ -44,6 +59,13 @@ class NoteService
}
}
if (array_key_exists('checkin', $data) && $data['checkin'] !== null) {
$place = Place::where('foursquare', $data['checkin'])->first();
if ($place !== null) {
$note->place()->associate($place);
}
}
/* drop image support for now
//add images to media library
if ($request->hasFile('photo')) {
@ -55,12 +77,17 @@ class NoteService
*/
//add support for media uploaded as URLs
foreach ($data['photo'] as $photo) {
// check the media was uploaded to my endpoint
// check the media was uploaded to my endpoint, and use path
if (starts_with($photo, config('filesystems.disks.s3.url'))) {
$path = substr($photo, strlen(config('filesystems.disks.s3.url')));
$media = Media::where('path', ltrim($path, '/'))->firstOrFail();
$note->media()->save($media);
} else {
$media = Media::firstOrNew(['path' => $photo]);
// currently assuming this is a photo from Swarm
$media->type = 'image';
$media->save();
}
$note->media()->save($media);
}
$note->save();

View file

@ -36,4 +36,39 @@ class PlaceService
return $place;
}
/**
* Create a place from a h-card checkin, for exameple from OwnYourSwarm.
*
* @param array
* @return bool
*/
public function createPlaceFromCheckin(array $checkin): bool
{
//check if the place exists if from swarm
if (array_key_exists('url', $checkin['properties'])) {
$search = Place::where('foursquare', $checkin['properties']['url'][0])->count();
if ($search === 1) {
return true;
}
}
if (array_key_exists('name', $checkin['properties']) === false) {
throw new \InvalidArgumentException('Missing required name');
}
if (array_key_exists('latitude', $checkin['properties']) === false) {
throw new \InvalidArgumentException('Missing required longitude/latitude');
}
$place = new Place();
$place->name = $checkin['properties']['name'][0];
if (starts_with($checkin['properties']['url'][0], 'https://foursquare.com')) {
$place->foursquare = $checkin['properties']['url'][0];
}
$place->location = new Point(
(float) $checkin['properties']['latitude'][0],
(float) $checkin['properties']['longitude'][0]
);
$place->save();
return true;
}
}

View file

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

View file

@ -1,5 +1,10 @@
# 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)
- 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

View file

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

381
composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.24.7",
"version": "3.27.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "f062d7ea2123fe2aefef91da855c10ef8ff3af1c"
"reference": "eb10e43cccf8e868f9622ab8ce2beb9fb756b5a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f062d7ea2123fe2aefef91da855c10ef8ff3af1c",
"reference": "f062d7ea2123fe2aefef91da855c10ef8ff3af1c",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eb10e43cccf8e868f9622ab8ce2beb9fb756b5a8",
"reference": "eb10e43cccf8e868f9622ab8ce2beb9fb756b5a8",
"shasum": ""
},
"require": {
@ -39,7 +39,7 @@
"ext-simplexml": "*",
"ext-spl": "*",
"nette/neon": "^2.3",
"phpunit/phpunit": "~4.0|~5.0",
"phpunit/phpunit": "^4.8.35|^5.4.0",
"psr/cache": "^1.0"
},
"suggest": {
@ -84,7 +84,7 @@
"s3",
"sdk"
],
"time": "2017-03-23T22:17:20+00:00"
"time": "2017-05-11T21:23:43+00:00"
},
{
"name": "barnabywalters/mf-cleaner",
@ -180,6 +180,65 @@
],
"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",
"version": "0.1",
@ -685,16 +744,16 @@
},
{
"name": "erusev/parsedown",
"version": "1.6.1",
"version": "1.6.2",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
"reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb"
"reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/erusev/parsedown/zipball/20ff8bbb57205368b4b42d094642a3e52dac85fb",
"reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb",
"url": "https://api.github.com/repos/erusev/parsedown/zipball/1bf24f7334fe16c88bf9d467863309ceaf285b01",
"reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01",
"shasum": ""
},
"require": {
@ -723,7 +782,7 @@
"markdown",
"parser"
],
"time": "2016-11-02T15:56:58+00:00"
"time": "2017-03-29T16:04:15+00:00"
},
{
"name": "ezyang/htmlpurifier",
@ -1045,16 +1104,16 @@
},
{
"name": "indieauth/client",
"version": "0.2.0",
"version": "0.2.1",
"source": {
"type": "git",
"url": "https://github.com/indieweb/indieauth-client-php.git",
"reference": "4b9bd766a92b8abbe420f5889bf7ebac7678151d"
"reference": "f5f6efad79334d1ff9370fe4dce8ccf4814820fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/indieweb/indieauth-client-php/zipball/4b9bd766a92b8abbe420f5889bf7ebac7678151d",
"reference": "4b9bd766a92b8abbe420f5889bf7ebac7678151d",
"url": "https://api.github.com/repos/indieweb/indieauth-client-php/zipball/f5f6efad79334d1ff9370fe4dce8ccf4814820fa",
"reference": "f5f6efad79334d1ff9370fe4dce8ccf4814820fa",
"shasum": ""
},
"require": {
@ -1080,7 +1139,7 @@
}
],
"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",
@ -1405,16 +1464,16 @@
},
{
"name": "laravel/framework",
"version": "v5.4.16",
"version": "v5.4.23",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "6cf379ec34d08bcdc9c7183e369a8fdf04ade80d"
"reference": "ad82327705658dbf5f0ce72805caa950dfbe150d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/6cf379ec34d08bcdc9c7183e369a8fdf04ade80d",
"reference": "6cf379ec34d08bcdc9c7183e369a8fdf04ade80d",
"url": "https://api.github.com/repos/laravel/framework/zipball/ad82327705658dbf5f0ce72805caa950dfbe150d",
"reference": "ad82327705658dbf5f0ce72805caa950dfbe150d",
"shasum": ""
},
"require": {
@ -1530,20 +1589,20 @@
"framework",
"laravel"
],
"time": "2017-03-21T19:34:41+00:00"
"time": "2017-05-11T20:10:35+00:00"
},
{
"name": "laravel/scout",
"version": "v3.0.2",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/scout.git",
"reference": "1ddb0fa6f165bf6a69864960102062e7cf3f989d"
"reference": "64d28db58a054174eadf1d4df38dad81ff7e68dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/scout/zipball/1ddb0fa6f165bf6a69864960102062e7cf3f989d",
"reference": "1ddb0fa6f165bf6a69864960102062e7cf3f989d",
"url": "https://api.github.com/repos/laravel/scout/zipball/64d28db58a054174eadf1d4df38dad81ff7e68dd",
"reference": "64d28db58a054174eadf1d4df38dad81ff7e68dd",
"shasum": ""
},
"require": {
@ -1590,7 +1649,7 @@
"laravel",
"search"
],
"time": "2017-03-01T14:37:40+00:00"
"time": "2017-04-09T00:54:26+00:00"
},
{
"name": "laravel/tinker",
@ -1710,16 +1769,16 @@
},
{
"name": "league/commonmark",
"version": "0.15.3",
"version": "0.15.4",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc"
"reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c8b43ee5821362216f8e9ac684f0f59de164edcc",
"reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c",
"reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c",
"shasum": ""
},
"require": {
@ -1775,20 +1834,20 @@
"markdown",
"parser"
],
"time": "2016-12-19T00:11:43+00:00"
"time": "2017-05-09T12:47:53+00:00"
},
{
"name": "league/flysystem",
"version": "1.0.37",
"version": "1.0.40",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "78b5cc4feb61a882302df4fbaf63b7662e5e4ccd"
"reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/78b5cc4feb61a882302df4fbaf63b7662e5e4ccd",
"reference": "78b5cc4feb61a882302df4fbaf63b7662e5e4ccd",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3828f0b24e2c1918bb362d57a53205d6dc8fde61",
"reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61",
"shasum": ""
},
"require": {
@ -1810,12 +1869,12 @@
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
"league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
"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-rackspace": "Allows you to use Rackspace Cloud Files",
"league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
"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",
"extra": {
@ -1858,25 +1917,25 @@
"sftp",
"storage"
],
"time": "2017-03-22T15:43:14+00:00"
"time": "2017-04-28T10:15:08+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
"version": "1.0.13",
"version": "1.0.15",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "dc56a8faf3aff0841f9eae04b6af94a50657896c"
"reference": "c947f36f977b495a57e857ae1630df0da35ec456"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/dc56a8faf3aff0841f9eae04b6af94a50657896c",
"reference": "dc56a8faf3aff0841f9eae04b6af94a50657896c",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c947f36f977b495a57e857ae1630df0da35ec456",
"reference": "c947f36f977b495a57e857ae1630df0da35ec456",
"shasum": ""
},
"require": {
"aws/aws-sdk-php": "^3.0.0",
"league/flysystem": "~1.0",
"league/flysystem": "^1.0.40",
"php": ">=5.5.0"
},
"require-dev": {
@ -1905,7 +1964,7 @@
}
],
"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",
@ -2658,16 +2717,16 @@
},
{
"name": "ramsey/uuid",
"version": "3.6.0",
"version": "3.6.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "0b7bdfb180e72c8d76e75a649ced67e392201458"
"reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/0b7bdfb180e72c8d76e75a649ced67e392201458",
"reference": "0b7bdfb180e72c8d76e75a649ced67e392201458",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"shasum": ""
},
"require": {
@ -2679,9 +2738,9 @@
},
"require-dev": {
"apigen/apigen": "^4.1",
"codeception/aspect-mock": "1.0.0",
"codeception/aspect-mock": "^1.0 | ^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",
"jakub-onderka/php-parallel-lint": "^0.9.0",
"mockery/mockery": "^0.9.4",
@ -2736,23 +2795,24 @@
"identifier",
"uuid"
],
"time": "2017-03-18T15:38:09+00:00"
"time": "2017-03-26T20:37:53+00:00"
},
{
"name": "sensiolabs/security-checker",
"version": "v4.0.2",
"version": "v4.0.4",
"source": {
"type": "git",
"url": "https://github.com/sensiolabs/security-checker.git",
"reference": "56bded66985e22f6eac2cf86735fd21c625bff2f"
"reference": "9e69eddf3bc49d1ee5c7908564da3141796d4bbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sensiolabs/security-checker/zipball/56bded66985e22f6eac2cf86735fd21c625bff2f",
"reference": "56bded66985e22f6eac2cf86735fd21c625bff2f",
"url": "https://api.github.com/repos/sensiolabs/security-checker/zipball/9e69eddf3bc49d1ee5c7908564da3141796d4bbc",
"reference": "9e69eddf3bc49d1ee5c7908564da3141796d4bbc",
"shasum": ""
},
"require": {
"composer/ca-bundle": "^1.0",
"symfony/console": "~2.7|~3.0"
},
"bin": [
@ -2780,20 +2840,20 @@
}
],
"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",
"version": "v5.4.6",
"version": "v5.4.8",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e"
"reference": "9a06dc570a0367850280eefd3f1dc2da45aef517"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e",
"reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/9a06dc570a0367850280eefd3f1dc2da45aef517",
"reference": "9a06dc570a0367850280eefd3f1dc2da45aef517",
"shasum": ""
},
"require": {
@ -2834,20 +2894,20 @@
"mail",
"mailer"
],
"time": "2017-02-13T07:52:53+00:00"
"time": "2017-05-01T15:54:03+00:00"
},
{
"name": "symfony/console",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "28fb243a2b5727774ca309ec2d92da240f1af0dd"
"reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/28fb243a2b5727774ca309ec2d92da240f1af0dd",
"reference": "28fb243a2b5727774ca309ec2d92da240f1af0dd",
"url": "https://api.github.com/repos/symfony/console/zipball/a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38",
"reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38",
"shasum": ""
},
"require": {
@ -2897,20 +2957,20 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2017-03-06T19:30:27+00:00"
"time": "2017-04-26T01:39:17+00:00"
},
{
"name": "symfony/css-selector",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "a48f13dc83c168f1253a5d2a5a4fb46c36244c4c"
"reference": "02983c144038e697c959e6b06ef6666de759ccbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/a48f13dc83c168f1253a5d2a5a4fb46c36244c4c",
"reference": "a48f13dc83c168f1253a5d2a5a4fb46c36244c4c",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/02983c144038e697c959e6b06ef6666de759ccbc",
"reference": "02983c144038e697c959e6b06ef6666de759ccbc",
"shasum": ""
},
"require": {
@ -2950,20 +3010,20 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"time": "2017-02-21T09:12:04+00:00"
"time": "2017-05-01T14:55:58+00:00"
},
{
"name": "symfony/debug",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "b90c9f91ad8ac37d9f114e369042d3226b34dc1a"
"reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/b90c9f91ad8ac37d9f114e369042d3226b34dc1a",
"reference": "b90c9f91ad8ac37d9f114e369042d3226b34dc1a",
"url": "https://api.github.com/repos/symfony/debug/zipball/fd6eeee656a5a7b384d56f1072243fe1c0e81686",
"reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686",
"shasum": ""
},
"require": {
@ -3007,20 +3067,20 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2017-02-18T17:28:00+00:00"
"time": "2017-04-19T20:17:50+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "b7a1b9e0a0f623ce43b4c8d775eb138f190c9d8d"
"reference": "b8a401f733b43251e1d088c589368b2a94155e40"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7a1b9e0a0f623ce43b4c8d775eb138f190c9d8d",
"reference": "b7a1b9e0a0f623ce43b4c8d775eb138f190c9d8d",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b8a401f733b43251e1d088c589368b2a94155e40",
"reference": "b8a401f733b43251e1d088c589368b2a94155e40",
"shasum": ""
},
"require": {
@ -3067,20 +3127,20 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
"time": "2017-02-21T09:12:04+00:00"
"time": "2017-05-01T14:58:48+00:00"
},
{
"name": "symfony/finder",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "92d7476d2df60cd851a3e13e078664b1deb8ce10"
"reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/92d7476d2df60cd851a3e13e078664b1deb8ce10",
"reference": "92d7476d2df60cd851a3e13e078664b1deb8ce10",
"url": "https://api.github.com/repos/symfony/finder/zipball/9cf076f8f492f4b1ffac40aae9c2d287b4ca6930",
"reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930",
"shasum": ""
},
"require": {
@ -3116,20 +3176,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2017-02-21T09:12:04+00:00"
"time": "2017-04-12T14:13:17+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "c57009887010eb4e58bfca2970314a5b820b24b9"
"reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/c57009887010eb4e58bfca2970314a5b820b24b9",
"reference": "c57009887010eb4e58bfca2970314a5b820b24b9",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/9de6add7f731e5af7f5b2e9c0da365e43383ebef",
"reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef",
"shasum": ""
},
"require": {
@ -3169,20 +3229,20 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
"time": "2017-03-04T12:23:14+00:00"
"time": "2017-05-01T14:55:58+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "bc909e85b8585c9edf043d0fca871308c41bb9b4"
"reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/bc909e85b8585c9edf043d0fca871308c41bb9b4",
"reference": "bc909e85b8585c9edf043d0fca871308c41bb9b4",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/46e8b209abab55c072c47d72d5cd1d62c0585e05",
"reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05",
"shasum": ""
},
"require": {
@ -3251,7 +3311,7 @@
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
"time": "2017-03-10T18:35:31+00:00"
"time": "2017-05-01T17:46:48+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@ -3314,16 +3374,16 @@
},
{
"name": "symfony/process",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "68bfa8c83f24c0ac04ea7193bcdcda4519f41892"
"reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/68bfa8c83f24c0ac04ea7193bcdcda4519f41892",
"reference": "68bfa8c83f24c0ac04ea7193bcdcda4519f41892",
"url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0",
"reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0",
"shasum": ""
},
"require": {
@ -3359,20 +3419,20 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2017-03-04T12:23:14+00:00"
"time": "2017-04-12T14:13:17+00:00"
},
{
"name": "symfony/routing",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "d6605f9a5767bc5bc4895e1c762ba93964608aee"
"reference": "5029745d6d463585e8b487dbc83d6333f408853a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/d6605f9a5767bc5bc4895e1c762ba93964608aee",
"reference": "d6605f9a5767bc5bc4895e1c762ba93964608aee",
"url": "https://api.github.com/repos/symfony/routing/zipball/5029745d6d463585e8b487dbc83d6333f408853a",
"reference": "5029745d6d463585e8b487dbc83d6333f408853a",
"shasum": ""
},
"require": {
@ -3434,20 +3494,20 @@
"uri",
"url"
],
"time": "2017-03-02T15:58:09+00:00"
"time": "2017-04-12T14:13:17+00:00"
},
{
"name": "symfony/translation",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "0e1b15ce8fbf3890f4ccdac430ed5e07fdfe0690"
"reference": "f4a04d2df710f81515df576b2de06bdeee518b83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/0e1b15ce8fbf3890f4ccdac430ed5e07fdfe0690",
"reference": "0e1b15ce8fbf3890f4ccdac430ed5e07fdfe0690",
"url": "https://api.github.com/repos/symfony/translation/zipball/f4a04d2df710f81515df576b2de06bdeee518b83",
"reference": "f4a04d2df710f81515df576b2de06bdeee518b83",
"shasum": ""
},
"require": {
@ -3498,20 +3558,20 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2017-03-04T12:23:14+00:00"
"time": "2017-04-12T14:13:17+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "4100f347aff890bc16b0b4b42843b599db257b2d"
"reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/4100f347aff890bc16b0b4b42843b599db257b2d",
"reference": "4100f347aff890bc16b0b4b42843b599db257b2d",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/fa47963ac7979ddbd42b2d646d1b056bddbf7bb8",
"reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8",
"shasum": ""
},
"require": {
@ -3522,9 +3582,11 @@
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
},
"require-dev": {
"ext-iconv": "*",
"twig/twig": "~1.20|~2.0"
},
"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": ""
},
"type": "library",
@ -3564,7 +3626,7 @@
"debug",
"dump"
],
"time": "2017-02-20T13:45:48+00:00"
"time": "2017-05-01T14:55:58+00:00"
},
{
"name": "themattharris/tmhoauth",
@ -3610,16 +3672,16 @@
},
{
"name": "thujohn/twitter",
"version": "2.2.2",
"version": "2.2.5",
"source": {
"type": "git",
"url": "https://github.com/thujohn/twitter.git",
"reference": "203d903856212835206675ae9c0504d74b681886"
"reference": "ff414bdadba3f1570ca211355e5359ec266552d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thujohn/twitter/zipball/203d903856212835206675ae9c0504d74b681886",
"reference": "203d903856212835206675ae9c0504d74b681886",
"url": "https://api.github.com/repos/thujohn/twitter/zipball/ff414bdadba3f1570ca211355e5359ec266552d8",
"reference": "ff414bdadba3f1570ca211355e5359ec266552d8",
"shasum": ""
},
"require": {
@ -3650,7 +3712,7 @@
"laravel5",
"twitter"
],
"time": "2017-01-31T23:37:52+00:00"
"time": "2017-04-27T09:00:04+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@ -3861,16 +3923,16 @@
},
{
"name": "facebook/webdriver",
"version": "1.4.0",
"version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/facebook/php-webdriver.git",
"reference": "3ea034c056189e11c0ce7985332a9f4b5b2b5db2"
"reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/facebook/php-webdriver/zipball/3ea034c056189e11c0ce7985332a9f4b5b2b5db2",
"reference": "3ea034c056189e11c0ce7985332a9f4b5b2b5db2",
"url": "https://api.github.com/repos/facebook/php-webdriver/zipball/eadb0b7a7c3e6578185197fd40158b08c3164c83",
"reference": "eadb0b7a7c3e6578185197fd40158b08c3164c83",
"shasum": ""
},
"require": {
@ -3909,7 +3971,7 @@
"selenium",
"webdriver"
],
"time": "2017-03-22T10:56:03+00:00"
"time": "2017-04-28T14:54:49+00:00"
},
{
"name": "fzaninotto/faker",
@ -4053,21 +4115,22 @@
},
{
"name": "laravel/dusk",
"version": "v1.0.10",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
"reference": "11537ac1a939a2194e9e3cdc2536e6e34eff9ea6"
"reference": "6b81e97ae1ce384e3d8dbd020b2b9751c1449889"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/dusk/zipball/11537ac1a939a2194e9e3cdc2536e6e34eff9ea6",
"reference": "11537ac1a939a2194e9e3cdc2536e6e34eff9ea6",
"url": "https://api.github.com/repos/laravel/dusk/zipball/6b81e97ae1ce384e3d8dbd020b2b9751c1449889",
"reference": "6b81e97ae1ce384e3d8dbd020b2b9751c1449889",
"shasum": ""
},
"require": {
"facebook/webdriver": "~1.0",
"illuminate/support": "~5.3",
"illuminate/console": "~5.4",
"illuminate/support": "~5.4",
"nesbot/carbon": "~1.20",
"php": ">=5.6.4",
"symfony/console": "~3.2",
@ -4104,7 +4167,7 @@
"testing",
"webdriver"
],
"time": "2017-03-03T14:36:19+00:00"
"time": "2017-04-23T17:13:04+00:00"
},
{
"name": "maximebf/debugbar",
@ -4172,12 +4235,12 @@
"version": "0.9.9",
"source": {
"type": "git",
"url": "https://github.com/padraic/mockery.git",
"url": "https://github.com/mockery/mockery.git",
"reference": "6fdb61243844dc924071d3404bb23994ea0b6856"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/padraic/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856",
"url": "https://api.github.com/repos/mockery/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856",
"reference": "6fdb61243844dc924071d3404bb23994ea0b6856",
"shasum": ""
},
@ -4234,16 +4297,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.6.0",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe"
"reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe",
"reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102",
"reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102",
"shasum": ""
},
"require": {
@ -4272,7 +4335,7 @@
"object",
"object graph"
],
"time": "2017-01-26T22:05:40+00:00"
"time": "2017-04-12T18:52:22+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@ -4485,16 +4548,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "4.0.7",
"version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "09e2277d14ea467e5a984010f501343ef29ffc69"
"reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/09e2277d14ea467e5a984010f501343ef29ffc69",
"reference": "09e2277d14ea467e5a984010f501343ef29ffc69",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
"reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
"shasum": ""
},
"require": {
@ -4544,7 +4607,7 @@
"testing",
"xunit"
],
"time": "2017-03-01T09:12:17+00:00"
"time": "2017-04-02T07:44:40+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -4734,16 +4797,16 @@
},
{
"name": "phpunit/phpunit",
"version": "5.7.17",
"version": "5.7.19",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "68752b665d3875f9a38a357e3ecb35c79f8673bf"
"reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/68752b665d3875f9a38a357e3ecb35c79f8673bf",
"reference": "68752b665d3875f9a38a357e3ecb35c79f8673bf",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1",
"reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1",
"shasum": ""
},
"require": {
@ -4812,7 +4875,7 @@
"testing",
"xunit"
],
"time": "2017-03-19T16:52:12+00:00"
"time": "2017-04-03T02:22:27+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
@ -5477,16 +5540,16 @@
},
{
"name": "symfony/yaml",
"version": "v3.2.6",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a"
"reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/093e416ad096355149e265ea2e4cc1f9ee40ab1a",
"reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a",
"url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6",
"reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6",
"shasum": ""
},
"require": {
@ -5528,20 +5591,20 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2017-03-07T16:47:02+00:00"
"time": "2017-05-01T14:55:58+00:00"
},
{
"name": "theseer/fdomdocument",
"version": "1.6.1",
"version": "1.6.5",
"source": {
"type": "git",
"url": "https://github.com/theseer/fDOMDocument.git",
"reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684"
"reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/d9ad139d6c2e8edf5e313ffbe37ff13344cf0684",
"reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684",
"url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/8dcfd392135a5bd938c3c83ea71419501ad9855d",
"reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d",
"shasum": ""
},
"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.",
"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",

View file

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

View file

@ -14,7 +14,7 @@ return [
*/
'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(NotesTableSeeder::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"
php artisan down
echo "Updating composer dependencies"
echo "Updating composer and dependencies"
composer self-update
composer install
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",
"dependencies": {
"alertify.js": "^1.0.12",
"mapbox-gl": "^0.34.0",
"mapbox-gl": "0.37.0",
"marked": "^0.3.6",
"normalize.css": "^5.0.0",
"normalize.css": "7.0.0",
"webStorage": "^1.2.2"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-core": "^6.21.0",
"babel-loader": "^6.2.10",
"babel-loader": "7.0.0",
"babel-preset-env": "^1.2.2",
"babel-preset-es2015": "^6.18.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,84 +1,79 @@
/* 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
* Based on the slides of the talk /Reg(exp){2}lained/
* @author Lea Verou
* okaidia theme for JavaScript, CSS and HTML
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
* @author ocodia
*/
code[class*="language-"],
pre[class*="language-"] {
color: white;
text-shadow: 0 -.1em .2em black;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-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%);
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border: .3em solid hsl(30, 20%, 40%);
border-radius: .5em;
box-shadow: 1px 1px .5em black inset;
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #272822;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .15em .2em .05em;
border-radius: .3em;
border: .13em solid hsl(30, 20%, 40%);
box-shadow: 1px 1px .3em -.1em black inset;
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: hsl(30, 20%, 50%);
color: slategray;
}
.token.punctuation {
opacity: .7;
color: #f8f8f2;
}
.namespace {
opacity: .7;
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol {
color: hsl(350, 40%, 70%);
.token.symbol,
.token.deleted {
color: #f92672;
}
.token.boolean,
.token.number {
color: #ae81ff;
}
.token.selector,
@ -87,7 +82,7 @@ pre[class*="language-"] {
.token.char,
.token.builtin,
.token.inserted {
color: hsl(75, 70%, 60%);
color: #a6e22e;
}
.token.operator,
@ -96,93 +91,76 @@ pre[class*="language-"] {
.language-css .token.string,
.style .token.string,
.token.variable {
color: hsl(40, 90%, 60%);
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function {
color: #e6db74;
}
.token.keyword {
color: hsl(350, 40%, 70%);
color: #66d9ef;
}
.token.regex,
.token.important {
color: #e90;
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
font-weight: bold;
}
.token.italic {
font-style: italic;
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.deleted {
color: red;
cursor: help;
}
pre.line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
position: relative;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 1px solid #999;
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 1px solid #999;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #999;
display: block;
padding-right: 0.8em;
text-align: right;
}
.token.tab:not(:empty):before,
.token.cr:before,
.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';
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #999;
display: block;
padding-right: 0.8em;
text-align: right;
}
.token a {
color: inherit;
}

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')
<p>The values for your micropub endpoint.</p>
<dl>
<dt>Me (your url)</dt><dd>{{ $data['me'] }}</dd>
<dt>Token</dt><dd>{{ $data['token'] }}</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>Media Endpoint</dt><dd>{{ $data['media-endpoint'] }}</dd>
<dt>Me (your url)</dt><dd><code>{{ $data['me'] }}</code></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>@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>
<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>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

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}/{slug}', 'ArticlesController@show');
//micropub new notes page
//this needs to be first so `notes/new` doesn't match `notes/{id}`
//Notes pages using NotesController
Route::get('notes', 'NotesController@index');
Route::get('notes/{id}', 'NotesController@show');
Route::get('note/{id}', 'NotesController@redirect');
Route::get('notes/tagged/{tag}', 'NotesController@tagged');
//indieauth
// IndieAuth
Route::post('indieauth/start', 'IndieAuthController@start')->name('indieauth-start');
Route::get('indieauth/callback', 'IndieAuthController@callback')->name('indieauth-callback');
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
Route::get('micropub/create', 'MicropubClientController@create')->name('micropub-client');
Route::post('micropub', 'MicropubClientController@store')->name('micropub-client-post');
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::post('micropub/update-syntax', 'MicropubClientController@updateSyntax')->name('micropub-update-syntax');
Route::get('micropub/places', 'MicropubClientController@nearbyPlaces');
Route::post('micropub/places', 'MicropubClientController@newPlace');
Route::post('micropub/media', 'MicropubClientController@processMedia')->name('process-media');
Route::get('micropub/media/clearlinks', 'MicropubClientController@clearLinks');
// Micropub Endpoint
Route::get('api/post', 'MicropubController@get');
Route::post('api/post', 'MicropubController@post');
Route::post('api/media', 'MicropubController@media')->name('media-endpoint');
// Micropub Endpoints
Route::get('api/post', 'MicropubController@get')->middleware('micropub.token');
Route::post('api/post', 'MicropubController@post')->middleware('micropub.token');
Route::post('api/media', 'MicropubController@media')->middleware('micropub.token')->name('media-endpoint');
//webmention
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()
{
\Artisan::call('token:generate');
$faker = \Faker\Factory::create();
$note = 'Fake note from #LaravelDusk: ' . $faker->text;
$this->browse(function ($browser) use ($note) {
$browser->visit(route('micropub-client'))
->assertSeeLink('log out')
->type('content', $note)
->press('Submit');
});
sleep(2);
$this->assertDatabaseHas('notes', ['note' => $note]);
$newNote = \App\Note::where('note', $note)->first();
$newNote->forceDelete();

View file

@ -19,11 +19,11 @@ class MicropubControllerTest extends TestCase
*
* @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->assertStatus(400);
$response->assertJsonFragment(['error_description' => 'No token provided with request']);
$response->assertStatus(401);
$response->assertJsonFragment(['error_description' => 'No access token was provided in the request']);
}
/**
@ -202,9 +202,9 @@ class MicropubControllerTest extends TestCase
$response
->assertJson([
'response' => 'error',
'error' => 'no_token'
'error' => 'unauthorized'
])
->assertStatus(400);
->assertStatus(401);
}
/**
@ -231,9 +231,9 @@ class MicropubControllerTest extends TestCase
$response
->assertJson([
'response' => 'error',
'error' => 'invalid_token'
'error' => 'insufficient_scope'
])
->assertStatus(400);
->assertStatus(401);
}
public function test_micropub_request_with_json_syntax_creates_new_place()
@ -276,6 +276,47 @@ class MicropubControllerTest extends TestCase
->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.
*
@ -287,7 +328,7 @@ class MicropubControllerTest extends TestCase
$token = (new Builder())
->set('client_id', 'https://quill.p3k.io')
->set('me', 'https://jonnybarnes.localhost')
->set('scope', 'post')
->set('scope', 'create update')
->set('issued_at', time())
->sign($signer, env('APP_KEY'))
->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]
error_log = /tmp/php-fpm.error.log
[travis]
user = {USER}

885
yarn.lock

File diff suppressed because it is too large Load diff