Merge branch 'release/0.14'

This commit is contained in:
Jonny Barnes 2017-12-22 15:38:43 +00:00
commit f8cb9eb4fb
181 changed files with 5829 additions and 2253 deletions

View file

@ -1,9 +1,9 @@
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:6DJhvZLVjE6dD4Cqrteh+6Z5vZlG+v/soCKcDHLOAH0=
APP_URL=http://localhost:8000
APP_LONGURL=localhost
APP_SHORTURL=local
APP_URL=http://jonnybarnes.localhost:8000
APP_LONGURL=jonnybarnes.localhost
APP_SHORTURL=jmb.localhost
DB_CONNECTION=travis

View file

@ -7,12 +7,13 @@ cache:
- apt
addons:
hosts:
- jmb.localhost
- jonnybarnes.localhost
postgresql: "9.6"
apt:
sources:
- sourceline: 'deb http://ppa.launchpad.net/nginx/development/ubuntu trusty main'
packages:
- nginx
- nginx-full
- realpath
- postgresql-9.6-postgis-2.3
- imagemagick
@ -36,10 +37,6 @@ php:
- 7.1
- 7.2
matrix:
allow_failures:
- php: 7.2
before_install:
- printf "\n" | pecl install imagick
- cp .env.travis .env
@ -47,6 +44,7 @@ before_install:
- psql -U travis -c 'create database travis_ci_test'
- psql -U travis -d travis_ci_test -c 'create extension postgis'
- travis_retry composer self-update --preview
- pear install pear/PHP_CodeSniffer && phpenv rehash
install:
- if [[ $setup = 'basic' ]]; then travis_retry composer install --no-interaction --prefer-dist; fi
@ -66,6 +64,10 @@ before_script:
#- sleep 5
script:
- php vendor/bin/phpunit --coverage-text
- php vendor/bin/phpunit --coverage-clover build/logs/clover.xml
- phpcs
#- php artisan dusk
- php vendor/bin/security-checker security:check ./composer.lock --end-point=http://security.sensiolabs.org/check_lock
- php vendor/bin/security-checker security:check --end-point=http://security.sensiolabs.org/check_lock
after_success:
- travis_retry php vendor/bin/coveralls

View file

@ -2,7 +2,7 @@
namespace App\Console\Commands;
use App\WebMention;
use App\Models\WebMention;
use Illuminate\Console\Command;
use Illuminate\FileSystem\FileSystem;

View file

@ -2,7 +2,7 @@
namespace App\Console\Commands;
use App\WebMention;
use App\Models\WebMention;
use Illuminate\Console\Command;
use App\Jobs\DownloadWebMention;

View file

@ -5,6 +5,9 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use SensioLabs\Security\SecurityChecker;
/**
* @codeCoverageIgnore
*/
class SecurityCheck extends Command
{
/**

View file

@ -7,6 +7,9 @@ use Illuminate\Support\Facades\Route;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
/**
* @codeCoverageIgnore
*/
class Handler extends ExceptionHandler
{
/**

View file

@ -1,10 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class InternetArchiveErrorSavingException extends Exception
{
//
}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions;
class InternetArchiveException extends \Exception
{
}

View file

@ -4,7 +4,7 @@ namespace App\Exceptions;
use Exception;
class RemoteContentNotFound extends Exception
class RemoteContentNotFoundException extends Exception
{
//used when guzzle cant find the remote content
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers\Admin;
use App\Article;
use App\Models\Article;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

View file

@ -2,8 +2,8 @@
namespace App\Http\Controllers\Admin;
use App\MicropubClient;
use Illuminate\Http\Request;
use App\Models\MicropubClient;
use App\Http\Controllers\Controller;
class ClientsController extends Controller
@ -86,9 +86,9 @@ class ClientsController extends Controller
* @param string The client id
* @return redirect
*/
public function destroy($articleId)
public function destroy($clientId)
{
MicropubClient::where('id', $articleId)->delete();
MicropubClient::where('id', $clientId)->delete();
return redirect('/admin/clients');
}

View file

@ -2,8 +2,8 @@
namespace App\Http\Controllers\Admin;
use App\Contact;
use GuzzleHttp\Client;
use App\Models\Contact;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Filesystem\Filesystem;
@ -83,16 +83,14 @@ class ContactsController extends Controller
$contact->facebook = $request->input('facebook');
$contact->save();
if ($request->hasFile('avatar')) {
if ($request->input('homepage') != '') {
$dir = parse_url($request->input('homepage'))['host'];
$destination = public_path() . '/assets/profile-images/' . $dir;
$filesystem = new Filesystem();
if ($filesystem->isDirectory($destination) === false) {
$filesystem->makeDirectory($destination);
}
$request->file('avatar')->move($destination, 'image');
if ($request->hasFile('avatar') && ($request->input('homepage') != '')) {
$dir = parse_url($request->input('homepage'), PHP_URL_HOST);
$destination = public_path() . '/assets/profile-images/' . $dir;
$filesystem = new Filesystem();
if ($filesystem->isDirectory($destination) === false) {
$filesystem->makeDirectory($destination);
}
$request->file('avatar')->move($destination, 'image');
}
return redirect('/admin/contacts');
@ -123,37 +121,47 @@ class ContactsController extends Controller
*/
public function getAvatar($contactId)
{
// Initialising
$avatarURL = null;
$avatar = null;
$contact = Contact::findOrFail($contactId);
$homepage = $contact->homepage;
if (($homepage !== null) && ($homepage !== '')) {
$client = new Client();
if (mb_strlen($contact->homepage !== null) !== 0) {
$client = resolve(Client::class);
try {
$response = $client->get($homepage);
$html = (string) $response->getBody();
$mf2 = \Mf2\parse($html, $homepage);
$response = $client->get($contact->homepage);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
return "Bad Response from $homepage";
return redirect('/admin/contacts/' . $contactId . '/edit')
->with('error', 'Bad resposne from contacts homepage');
}
$avatarURL = null; // Initialising
$mf2 = \Mf2\parse((string) $response->getBody(), $contact->homepage);
foreach ($mf2['items'] as $microformat) {
if ($microformat['type'][0] == 'h-card') {
$avatarURL = $microformat['properties']['photo'][0];
if (array_get($microformat, 'type.0') == 'h-card') {
$avatarURL = array_get($microformat, 'properties.photo.0');
break;
}
}
try {
$avatar = $client->get($avatarURL);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
return "Unable to get $avatarURL";
if ($avatarURL !== null) {
try {
$avatar = $client->get($avatarURL);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
return redirect('/admin/contacts/' . $contactId . '/edit')
->with('error', 'Unable to download avatar');
}
}
$directory = public_path() . '/assets/profile-images/' . parse_url($homepage)['host'];
$filesystem = new Filesystem();
if ($filesystem->isDirectory($directory) === false) {
$filesystem->makeDirectory($directory);
}
$filesystem->put($directory . '/image', $avatar->getBody());
if ($avatar !== null) {
$directory = public_path() . '/assets/profile-images/' . parse_url($contact->homepage, PHP_URL_HOST);
$filesystem = new Filesystem();
if ($filesystem->isDirectory($directory) === false) {
$filesystem->makeDirectory($directory);
}
$filesystem->put($directory . '/image', $avatar->getBody());
return view('admin.contacts.getavatarsuccess', ['homepage' => parse_url($homepage)['host']]);
return view('admin.contacts.getavatarsuccess', [
'homepage' => parse_url($contact->homepage, PHP_URL_HOST),
]);
}
}
return redirect('/admin/contacts/' . $contactId . '/edit');
}
}

View file

@ -2,22 +2,13 @@
namespace App\Http\Controllers\Admin;
use App\Note;
use Validator;
use App\Models\Note;
use Illuminate\Http\Request;
use App\Jobs\SendWebMentions;
use App\Services\NoteService;
use App\Http\Controllers\Controller;
class NotesController extends Controller
{
protected $noteService;
public function __construct(NoteService $noteService)
{
$this->noteService = $noteService;
}
/**
* List the notes that can be edited.
*
@ -51,30 +42,10 @@ class NotesController extends Controller
*/
public function store(Request $request)
{
$validator = Validator::make(
$request->all(),
['photo' => 'photosize'],
['photosize' => 'At least one uploaded file exceeds size limit of 5MB']
);
if ($validator->fails()) {
return redirect('/admin/notes/create')
->withErrors($validator)
->withInput();
}
$data = [];
$data['content'] = $request->input('content');
$data['in-reply-to'] = $request->input('in-reply-to');
$data['location'] = $request->input('location');
$data['syndicate'] = [];
if ($request->input('twitter')) {
$data['syndicate'][] = 'twitter';
}
if ($request->input('facebook')) {
$data['syndicate'][] = 'facebook';
}
$note = $this->noteService->createNote($data);
Note::create([
'in-reply-to' => $request->input('in-reply-to'),
'note' => $request->input('content'),
]);
return redirect('/admin/notes');
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers\Admin;
use App\Place;
use App\Models\Place;
use Illuminate\Http\Request;
use App\Services\PlaceService;
use App\Http\Controllers\Controller;
@ -47,11 +47,7 @@ class PlacesController extends Controller
*/
public function store(Request $request)
{
$data = [];
$data['name'] = $request->name;
$data['description'] = $request->description;
$data['latitude'] = $request->latitude;
$data['longitude'] = $request->longitude;
$data = $request->only(['name', 'description', 'latitude', 'longitude']);
$place = $this->placeService->createPlace($data);
return redirect('/admin/places');
@ -67,14 +63,7 @@ class PlacesController extends Controller
{
$place = Place::findOrFail($placeId);
return view('admin.places.edit', [
'id' => $placeId,
'name' => $place->name,
'description' => $place->description,
'latitude' => $place->latitude,
'longitude' => $place->longitude,
'icon' => $place->icon ?? 'marker',
]);
return view('admin.places.edit', compact('place'));
}
/**

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Article;
use App\Models\Article;
use Jonnybarnes\IndieWeb\Numbers;
class ArticlesController extends Controller
@ -15,7 +15,7 @@ class ArticlesController extends Controller
public function index($year = null, $month = null)
{
$articles = Article::where('published', '1')
->date($year, $month)
->date((int) $year, (int) $month)
->orderBy('updated_at', 'desc')
->simplePaginate(5);
@ -31,7 +31,7 @@ class ArticlesController extends Controller
{
$article = Article::where('titleurl', $slug)->firstOrFail();
if ($article->updated_at->year != $year || $article->updated_at->month != $month) {
throw new \Exception;
return redirect('/blog/' . $article->updated_at->year . '/' . $article->updated_at->month .'/' . $slug);
}
return view('articles.show', compact('article'));

View file

@ -1,32 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
}

View file

@ -1,39 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
}

View file

@ -1,39 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Bookmark;
use App\Models\Bookmark;
class BookmarksController extends Controller
{

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Contact;
use App\Models\Contact;
use Illuminate\Filesystem\Filesystem;
class ContactsController extends Controller

View file

@ -2,8 +2,7 @@
namespace App\Http\Controllers;
use App\Note;
use App\Article;
use App\Models\{Article, Note};
class FeedsController extends Controller
{

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Like;
use App\Models\Like;
class LikesController extends Controller
{

View file

@ -2,320 +2,96 @@
namespace App\Http\Controllers;
use Storage;
use Monolog\Logger;
use Ramsey\Uuid\Uuid;
use App\Jobs\ProcessImage;
use App\Services\LikeService;
use App\Services\BookmarkService;
use App\Jobs\ProcessMedia;
use App\Services\TokenService;
use Illuminate\Http\UploadedFile;
use Monolog\Handler\StreamHandler;
use App\{Like, Media, Note, Place};
use Intervention\Image\ImageManager;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\{Request, Response};
use App\Exceptions\InvalidTokenException;
use App\Models\{Like, Media, Note, Place};
use Phaza\LaravelPostgis\Geometries\Point;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
use App\Services\{NoteService, PlaceService, TokenService};
use Intervention\Image\Exception\NotReadableException;
use App\Services\Micropub\{HCardService, HEntryService, UpdateService};
class MicropubController extends Controller
{
/**
* The Token service container.
*/
protected $tokenService;
protected $hentryService;
protected $hcardService;
protected $updateService;
/**
* The Note service container.
*/
protected $noteService;
/**
* The Place service container.
*/
protected $placeService;
/**
* Inject the dependencies.
*/
public function __construct(
TokenService $tokenService,
NoteService $noteService,
PlaceService $placeService
HEntryService $hentryService,
HCardService $hcardService,
UpdateService $updateService
) {
$this->tokenService = $tokenService;
$this->noteService = $noteService;
$this->placeService = $placeService;
$this->hentryService = $hentryService;
$this->hcardService = $hcardService;
$this->updateService = $updateService;
}
/**
* This function receives an API request, verifies the authenticity
* then passes over the info to the relavent Service class.
*
* @param \Illuminate\Http\Request request
* @return \Illuminate\Http\Response
*/
public function post(Request $request)
public function post()
{
try {
$tokenData = $this->tokenService->validateToken($request->bearerToken());
$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);
return $this->invalidTokenResponse();
}
// Log the request
$logger = new Logger('micropub');
$logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG);
$logger->debug('MicropubLog', $request->all());
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();
}
if ($request->has('properties.like-of') || $request->has('like-of')) {
$like = (new LikeService())->createLike($request);
return response()->json([
'response' => 'created',
'location' => config('app.url') . "/likes/$like->id",
], 201)->header('Location', config('app.url') . "/likes/$like->id");
}
if ($request->has('properties.bookmark-of') || $request->has('bookmark-of')) {
$bookmark = (new BookmarkService())->createBookmark($request);
if ($tokenData->hasClaim('scope') === false) {
return $this->tokenHasNoScopeResponse();
}
return response()->json([
'response' => 'created',
'location' => config('app.url') . "/bookmarks/$bookmark->id",
], 201)->header('Location', config('app.url') . "/bookmarks/$bookmark->id");
}
$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');
// check location is geo: string
if (is_string($request->input('properties.location.0'))) {
$data['location'] = $request->input('properties.location.0');
}
// check location is h-card
if (is_array($request->input('properties.location.0'))) {
if ($request->input('properties.location.0.type.0' === 'h-card')) {
try {
$place = $this->placeService->createPlaceFromCheckin(
$request->input('properties.location.0')
);
$data['checkin'] = $place->longurl;
} catch (\Exception $e) {
//
}
}
}
$data['published'] = $request->input('properties.published.0');
//create checkin place
if (array_key_exists('checkin', $request->input('properties'))) {
$data['swarm-url'] = $request->input('properties.syndication.0');
try {
$place = $this->placeService->createPlaceFromCheckin(
$request->input('properties.checkin.0')
);
$data['checkin'] = $place->longurl;
} catch (\Exception $e) {
$data['checkin'] = null;
$data['swarm-url'] = null;
}
}
} 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');
$mpSyndicateTo = null;
if ($request->has('mp-syndicate-to')) {
$mpSyndicateTo = $request->input('mp-syndicate-to');
}
if ($request->has('properties.mp-syndicate-to')) {
$mpSyndicateTo = $request->input('properties.mp-syndicate-to');
}
if (is_string($mpSyndicateTo)) {
$service = array_search($mpSyndicateTo, $targets);
if ($service == 'Twitter') {
$data['syndicate'][] = 'twitter';
}
if ($service == 'Facebook') {
$data['syndicate'][] = 'facebook';
}
}
if (is_array($mpSyndicateTo)) {
foreach ($mpSyndicateTo as $uid) {
$service = array_search($uid, $targets);
if ($service == 'Twitter') {
$data['syndicate'][] = 'twitter';
}
if ($service == 'Facebook') {
$data['syndicate'][] = 'facebook';
}
}
}
$data['photo'] = [];
$photos = null;
if ($request->has('photo')) {
$photos = $request->input('photo');
}
if ($request->has('properties.photo')) {
$photos = $request->input('properties.photo');
}
if ($photos !== null) {
foreach ($photos as $photo) {
if (is_string($photo)) {
//only supporting media URLs for now
$data['photo'][] = $photo;
}
}
if (starts_with($request->input('properties.syndication.0'), 'https://www.instagram.com')) {
$data['instagram-url'] = $request->input('properties.syndication.0');
}
}
try {
$note = $this->noteService->createNote($data);
} catch (\Exception $exception) {
return response()->json(['error' => true], 400);
}
$this->logMicropubRequest(request()->all());
return response()->json([
'response' => 'created',
'location' => $note->longurl,
], 201)->header('Location', $note->longurl);
if ((request()->input('h') == 'entry') || (request()->input('type.0') == 'h-entry')) {
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->insufficientScopeResponse();
}
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);
}
$location = $this->hentryService->process(request()->all(), $this->getCLientId());
return response()->json([
'response' => 'created',
'location' => $place->longurl,
], 201)->header('Location', $place->longurl);
return response()->json([
'response' => 'created',
'location' => $location,
], 201)->header('Location', $location);
}
if (request()->input('h') == 'card' || request()->input('type')[0] == 'h-card') {
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->insufficientScopeResponse();
}
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))->firstOrFail();
} catch (ModelNotFoundException $exception) {
return response()->json([
'error' => 'invalid_request',
'error_description' => 'No known note with given ID',
]);
}
//got the note, are we dealing with a “replace” request?
if ($request->has('replace')) {
foreach ($request->input('replace') as $property => $value) {
if ($property == 'content') {
$note->note = $value[0];
}
if ($property == 'syndication') {
foreach ($value as $syndicationURL) {
if (starts_with($syndicationURL, 'https://www.facebook.com')) {
$note->facebook_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://www.swarmapp.com')) {
$note->swarm_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://twitter.com')) {
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
}
}
}
}
$note->save();
$location = $this->hcardService->process(request()->all());
return response()->json([
'response' => 'updated',
]);
}
//how about “add”
if ($request->has('add')) {
foreach ($request->input('add') as $property => $value) {
if ($property == 'syndication') {
foreach ($value as $syndicationURL) {
if (starts_with($syndicationURL, 'https://www.facebook.com')) {
$note->facebook_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://www.swarmapp.com')) {
$note->swarm_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://twitter.com')) {
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
}
}
}
if ($property == 'photo') {
foreach ($value as $photoURL) {
if (start_with($photo, 'https://')) {
$media = new Media();
$media->path = $photoURL;
$media->type = 'image';
$media->save();
$note->media()->save($media);
}
}
}
}
$note->save();
return response()->json([
'response' => 'created',
'location' => $location,
], 201)->header('Location', $location);
}
return response()->json([
'response' => 'updated',
]);
}
}
if (request()->input('action') == 'update') {
if (stristr($tokenData->getClaim('scope'), 'update') === false) {
return $this->insufficientScopeResponse();
}
return $this->updateService->process(request()->all());
}
return response()->json([
'response' => 'error',
'error' => 'forbidden',
'error_description' => 'The token has no scopes',
], 403);
'error_description' => 'unsupported_request_type',
], 500);
}
/**
@ -324,47 +100,37 @@ class MicropubController extends Controller
* appropriately. Further if the request has the query parameter
* synidicate-to we respond with the known syndication endpoints.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function get(Request $request)
public function get()
{
try {
$tokenData = $this->tokenService->validateToken($request->bearerToken());
$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);
return $this->invalidTokenResponse();
}
//we have a valid token, is `syndicate-to` set?
if ($request->input('q') === 'syndicate-to') {
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') {
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:') {
if (substr(request()->input('q'), 0, 4) === 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$request->input('q'),
request()->input('q'),
$matches
);
$distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000;
$places = Place::near(new Point($matches[0][0], $matches[0][1]))->get();
foreach ($places as $place) {
$place->uri = config('app.url') . '/places/' . $place->slug;
}
return response()->json([
'response' => 'places',
@ -372,7 +138,7 @@ class MicropubController extends Controller
]);
}
//nope, just return the token
// default response is just to return the token data
return response()->json([
'response' => 'token',
'token' => [
@ -386,77 +152,25 @@ class MicropubController extends Controller
/**
* Process a media item posted to the media endpoint.
*
* @param Illuminate\Http\Request $request
* @return Illuminate\Http\Response
*/
public function media(Request $request)
public function media()
{
try {
$tokenData = $this->tokenService->validateToken($request->bearerToken());
$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);
return $this->invalidTokenResponse();
}
$logger = new Logger('micropub');
$logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG);
$logger->debug('MicropubMediaLog', $request->all());
//check post scope
if ($tokenData->hasClaim('scope')) {
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->returnInsufficientScopeResponse();
}
//check media valid
if ($request->hasFile('file') && $request->file('file')->isValid()) {
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);
}
if ($tokenData->hasClaim('scope') === false) {
return $this->tokenHasNoScopeResponse();
}
$size = $request->file('file')->getClientSize();
Storage::disk('local')->put($filename, $request->file('file')->openFile()->fread($size));
try {
Storage::disk('s3')->put('media/' . $filename, $request->file('file')->openFile()->fread($size));
} catch (Exception $e) { // which exception?
return response()->json([
'response' => 'error',
'error' => 'service_unavailable',
'error_description' => 'Unable to save media to S3',
], 503);
}
$manager = app()->make(ImageManager::class);
try {
$image = $manager->make($request->file('file'));
$width = $image->width();
} catch (\Intervention\Image\Exception\NotReadableException $exception) {
// not an image
$width = null;
}
$media = new Media();
$media->token = $request->bearerToken();
$media->path = 'media/' . $filename;
$media->type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType());
$media->image_widths = $width;
$media->save();
dispatch(new ProcessImage($filename));
return response()->json([
'response' => 'created',
'location' => $media->url,
], 201)->header('Location', $media->url);
}
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->insufficientScopeResponse();
}
if ((request()->hasFile('file') && request()->file('file')->isValid()) === false) {
return response()->json([
'response' => 'error',
'error' => 'invalid_request',
@ -464,11 +178,32 @@ class MicropubController extends Controller
], 400);
}
$this->logMicropubRequest(request()->all());
$filename = $this->saveFile(request()->file('file'));
$manager = resolve(ImageManager::class);
try {
$image = $manager->make(request()->file('file'));
$width = $image->width();
} catch (NotReadableException $exception) {
// not an image
$width = null;
}
$media = Media::create([
'token' => request()->bearerToken(),
'path' => 'media/' . $filename,
'type' => $this->getFileTypeFromMimeType(request()->file('file')->getMimeType()),
'image_widths' => $width,
]);
ProcessMedia::dispatch($filename);
return response()->json([
'response' => 'error',
'error' => 'invalid_request',
'error_description' => 'The provided token has no scopes',
], 400);
'response' => 'created',
'location' => $media->url,
], 201)->header('Location', $media->url);
}
/**
@ -495,6 +230,7 @@ class MicropubController extends Controller
$videoMimeTypes = [
'video/mp4',
'video/mpeg',
'video/ogg',
'video/quicktime',
'video/webm',
];
@ -515,7 +251,29 @@ class MicropubController extends Controller
return 'download';
}
private function returnInsufficientScopeResponse()
private function getClientId(): string
{
return resolve(TokenService::class)
->validateToken(request()->bearerToken())
->getClaim('client_id');
}
private function logMicropubRequest(array $request)
{
$logger = new Logger('micropub');
$logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG);
$logger->debug('MicropubLog', $request);
}
private function saveFile(UploadedFile $file)
{
$filename = Uuid::uuid4() . '.' . $file->extension();
Storage::disk('local')->put($filename, $file);
return $filename;
}
private function insufficientScopeResponse()
{
return response()->json([
'response' => 'error',
@ -523,4 +281,22 @@ class MicropubController extends Controller
'error_description' => 'The tokens scope does not have the necessary requirements.',
], 401);
}
private function invalidTokenResponse()
{
return response()->json([
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
}
private function tokenHasNoScopeResponse()
{
return response()->json([
'response' => 'error',
'error' => 'invalid_request',
'error_description' => 'The provided token has no scopes',
], 400);
}
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Note;
use App\Models\Note;
use Illuminate\Http\Request;
use Jonnybarnes\IndieWeb\Numbers;
use App\Services\ActivityStreamsService;

View file

@ -1,94 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Note;
use Imagine\Image\Box;
use Imagine\Gd\Imagine;
use Illuminate\Http\Request;
use Illuminate\Filesystem\Filesystem;
class PhotosController extends Controller
{
/**
* Image box size limit for resizing photos.
*/
public function __construct()
{
$this->imageResizeLimit = 800;
}
/**
* Save an uploaded photo to the image folder.
*
* @param \Illuminate\Http\Request $request
* @param string The associated notes nb60 ID
* @return bool
*/
public function saveImage(Request $request, $nb60id)
{
if ($request->hasFile('photo') !== true) {
return false;
}
$photoFilename = 'note-' . $nb60id;
$path = public_path() . '/assets/img/notes/';
$ext = $request->file('photo')->getClientOriginalExtension();
$photoFilename .= '.' . $ext;
$request->file('photo')->move($path, $photoFilename);
return true;
}
/**
* Prepare a photo for posting to twitter.
*
* @param string photo fileanme
* @return string small photo filename, or null
*/
public function makeSmallPhotoForTwitter($photoFilename)
{
$imagine = new Imagine();
$orig = $imagine->open(public_path() . '/assets/img/notes/' . $photoFilename);
$size = [$orig->getSize()->getWidth(), $orig->getSize()->getHeight()];
if ($size[0] > $this->imageResizeLimit || $size[1] > $this->imageResizeLimit) {
$filenameParts = explode('.', $photoFilename);
$preExt = count($filenameParts) - 2;
$filenameParts[$preExt] .= '-small';
$photoFilenameSmall = implode('.', $filenameParts);
$aspectRatio = $size[0] / $size[1];
$box = ($aspectRatio >= 1) ?
[$this->imageResizeLimit, (int) round($this->imageResizeLimit / $aspectRatio)]
:
[(int) round($this->imageResizeLimit * $aspectRatio), $this->imageResizeLimit];
$orig->resize(new Box($box[0], $box[1]))
->save(public_path() . '/assets/img/notes/' . $photoFilenameSmall);
return $photoFilenameSmall;
}
}
/**
* Get the image path for a note.
*
* @param string $nb60id
* @return string | null
*/
public function getPhotoPath($nb60id)
{
$filesystem = new Filesystem();
$photoDir = public_path() . '/assets/img/notes';
$files = $filesystem->files($photoDir);
foreach ($files as $file) {
$parts = explode('.', $file);
$name = $parts[0];
$dirs = explode('/', $name);
$actualname = last($dirs);
if ($actualname == 'note-' . $nb60id) {
$ext = $parts[1];
}
}
if (isset($ext)) {
return '/assets/img/notes/note-' . $nb60id . '.' . $ext;
}
}
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Place;
use App\Models\Place;
class PlacesController extends Controller
{

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Note;
use App\Models\Note;
use Illuminate\Http\Request;
class SearchController extends Controller
@ -10,16 +10,6 @@ class SearchController extends Controller
public function search(Request $request)
{
$notes = Note::search($request->terms)->paginate(10);
foreach ($notes as $note) {
$note->iso8601_time = $note->updated_at->toISO8601String();
$note->human_time = $note->updated_at->diffForHumans();
$photoURLs = [];
$photos = $note->getMedia();
foreach ($photos as $photo) {
$photoURLs[] = $photo->getUrl();
}
$note->photoURLs = $photoURLs;
}
return view('search', compact('notes'));
}

View file

@ -2,9 +2,6 @@
namespace App\Http\Controllers;
use App\ShortURL;
use Jonnybanres\IndieWeb\Numbers;
class ShortURLsController extends Controller
{
/*
@ -41,21 +38,11 @@ class ShortURLsController extends Controller
*
* @return \Illuminate\Routing\RedirectResponse redirect
*/
public function googlePLus()
public function googlePlus()
{
return redirect('https://plus.google.com/u/0/117317270900655269082/about');
}
/**
* Redirect from '/α' to an App.net profile.
*
* @return \Illuminate\Routing\Redirector redirect
*/
public function appNet()
{
return redirect('https://alpha.app.net/jonnybarnes');
}
/**
* Redirect a short url of this site out to a long one based on post type.
* Further redirects may happen.
@ -75,46 +62,4 @@ class ShortURLsController extends Controller
return redirect(config('app.url') . '/' . $type . '/' . $postId);
}
/**
* Redirect a saved short URL, this is generic.
*
* @param string The short URL id
* @return \Illuminate\Routing\Redirector redirect
*/
public function redirect($shortURLId)
{
$numbers = new Numbers();
$num = $numbers->b60tonum($shortURLId);
$shorturl = ShortURL::find($num);
$redirect = $shorturl->redirect;
return redirect($redirect);
}
/**
* I had an old redirect systme breifly, but cool URLs should still work.
*
* @param string URL ID
* @return \Illuminate\Routing\Redirector redirect
*/
public function oldRedirect($shortURLId)
{
$filename = base_path() . '/public/assets/old-shorturls.json';
$handle = fopen($filename, 'r');
$contents = fread($handle, filesize($filename));
$object = json_decode($contents);
foreach ($object as $key => $val) {
if ($shortURLId == $key) {
return redirect($val);
}
}
return 'This id was never used.
Old redirects are located at
<code>
<a href="https://jonnybarnes.net/assets/old-shorturls.json">old-shorturls.json</a>
</code>.';
}
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Note;
use App\Models\Note;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Jobs\ProcessWebMention;
@ -36,35 +36,32 @@ class WebMentionsController extends Controller
$path = parse_url($request->input('target'), PHP_URL_PATH);
$pathParts = explode('/', $path);
switch ($pathParts[1]) {
case 'notes':
//we have a note
$noteId = $pathParts[2];
$numbers = new Numbers();
try {
$note = Note::findOrFail($numbers->b60tonum($noteId));
dispatch(new ProcessWebMention($note, $request->input('source')));
} catch (ModelNotFoundException $e) {
return new Response('This note doesnt exist.', 400);
}
if ($pathParts[1] == 'notes') {
//we have a note
$noteId = $pathParts[2];
$numbers = new Numbers();
try {
$note = Note::findOrFail($numbers->b60tonum($noteId));
dispatch(new ProcessWebMention($note, $request->input('source')));
} catch (ModelNotFoundException $e) {
return new Response('This note doesnt exist.', 400);
}
return new Response(
'Webmention received, it will be processed shortly',
202
);
break;
case 'blog':
return new Response(
'I dont accept webmentions for blog posts yet.',
501
);
break;
default:
return new Response(
'Invalid request',
400
);
break;
return new Response(
'Webmention received, it will be processed shortly',
202
);
}
if ($pathParts[1] == 'blog') {
return new Response(
'I dont accept webmentions for blog posts yet.',
501
);
}
return new Response(
'Invalid request',
400
);
}
}

View file

@ -36,7 +36,6 @@ 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\LocalhostSessionMiddleware::class,
\App\Http\Middleware\ActivityStreamLinks::class,
],

View file

@ -1,36 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Storage;
class DevTokenMiddleware
{
/**
* Handle an incoming request.
*
* @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')]);
if (Storage::exists('dev-token')) {
session(['token' => Storage::get('dev-token')]);
} else {
$data = [
'me' => config('app.url'),
'client_id' => route('micropub-client'),
'scope' => 'post',
];
$tokenService = new \App\Services\TokenService();
session(['token' => $tokenService->getNewToken($data)]);
}
}
return $next($request);
}
}

View file

@ -5,6 +5,9 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
/**
* @codeCoverageIgnore
*/
class RedirectIfAuthenticated
{
/**

View file

@ -2,8 +2,8 @@
namespace App\Jobs;
use App\MicropubClient;
use Illuminate\Bus\Queueable;
use App\Models\MicropubClient;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

View file

@ -41,7 +41,7 @@ class DownloadWebMention implements ShouldQueue
//Laravel should catch and retry these automatically.
if ($response->getStatusCode() == '200') {
$filesystem = new \Illuminate\FileSystem\FileSystem();
$filename = storage_path() . '/HTML/' . $this->createFilenameFromURL($this->source);
$filename = storage_path('HTML') . '/' . $this->createFilenameFromURL($this->source);
//backup file first
$filenameBackup = $filename . '.' . date('Y-m-d') . '.backup';
if ($filesystem->exists($filename)) {

View file

@ -2,14 +2,14 @@
namespace App\Jobs;
use App\Bookmark;
use App\Models\Bookmark;
use Illuminate\Bus\Queueable;
use App\Services\BookmarkService;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Exceptions\InternetArchiveErrorSavingException;
use App\Exceptions\InternetArchiveException;
class ProcessBookmark implements ShouldQueue
{
@ -34,12 +34,12 @@ class ProcessBookmark implements ShouldQueue
*/
public function handle()
{
$uuid = (new BookmarkService())->saveScreenshot($this->bookmark->url);
$uuid = (resolve(BookmarkService::class))->saveScreenshot($this->bookmark->url);
$this->bookmark->screenshot = $uuid;
try {
$archiveLink = (new BookmarkService())->getArchiveLink($this->bookmark->url);
} catch (InternetArchiveErrorSavingException $e) {
$archiveLink = (resolve(BookmarkService::class))->getArchiveLink($this->bookmark->url);
} catch (InternetArchiveException $e) {
$archiveLink = null;
}
$this->bookmark->archive = $archiveLink;

View file

@ -2,7 +2,7 @@
namespace App\Jobs;
use App\Like;
use App\Models\Like;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
@ -44,8 +44,8 @@ class ProcessLike implements ShouldQueue
try {
$author = $authorship->findAuthor($mf2);
if (is_array($author)) {
$this->like->author_name = $author['name'];
$this->like->author_url = $author['url'];
$this->like->author_name = array_get($author, 'properties.name.0');
$this->like->author_url = array_get($author, 'properties.url.0');
}
if (is_string($author) && $author !== '') {
$this->like->author_name = $author;

View file

@ -2,16 +2,16 @@
namespace App\Jobs;
use Storage;
use Illuminate\Bus\Queueable;
use Intervention\Image\ImageManager;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Intervention\Image\Exception\NotReadableException;
class ProcessImage implements ShouldQueue
class ProcessMedia implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -34,6 +34,10 @@ class ProcessImage implements ShouldQueue
*/
public function handle(ImageManager $manager)
{
Storage::disk('s3')->put(
'media/' . $this->filename,
storage_path('app') . '/' . $this->filename
);
//open file
try {
$image = $manager->make(storage_path('app') . '/' . $this->filename);

View file

@ -4,8 +4,8 @@ namespace App\Jobs;
use Mf2;
use GuzzleHttp\Client;
use App\{Note, WebMention};
use Illuminate\Bus\Queueable;
use App\Models\{Note, WebMention};
use Jonnybarnes\WebmentionsParser\Parser;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -41,23 +41,25 @@ class ProcessWebMention implements ShouldQueue
*/
public function handle(Parser $parser, Client $guzzle)
{
$remoteContent = $this->getRemoteContent($this->source, $guzzle);
if ($remoteContent === null) {
try {
$response = $guzzle->request('GET', $this->source);
} catch (RequestException $e) {
throw new RemoteContentNotFoundException;
}
$microformats = Mf2\parse($remoteContent, $this->source);
$this->saveRemoteContent((string) $response->getBody(), $this->source);
$microformats = Mf2\parse((string) $response->getBody(), $this->source);
$webmentions = WebMention::where('source', $this->source)->get();
foreach ($webmentions as $webmention) {
//check webmention still references target
//we try each type of mention (reply/like/repost)
// check webmention still references target
// we try each type of mention (reply/like/repost)
if ($webmention->type == 'in-reply-to') {
if ($parser->checkInReplyTo($microformats, $this->note->longurl) == false) {
//it doesn't so delete
// it doesnt so delete
$webmention->delete();
return;
}
//webmenion is still a reply, so update content
// webmenion is still a reply, so update content
dispatch(new SaveProfileImage($microformats));
$webmention->mf2 = json_encode($microformats);
$webmention->save();
@ -66,25 +68,25 @@ class ProcessWebMention implements ShouldQueue
}
if ($webmention->type == 'like-of') {
if ($parser->checkLikeOf($microformats, $note->longurl) == false) {
//it doesn't so delete
// it doesnt so delete
$webmention->delete();
return;
} //note we don't need to do anything if it still is a like
} // note we dont need to do anything if it still is a like
}
if ($webmention->type == 'repost-of') {
if ($parser->checkRepostOf($microformats, $note->longurl) == false) {
//it doesn't so delete
// it doesnt so delete
$webmention->delete();
return;
} //again, we don't need to do anything if it still is a repost
} // again, we dont need to do anything if it still is a repost
}
}//foreach
}// foreach
//no wemention in db so create new one
// no webmention in the db so create new one
$webmention = new WebMention();
$type = $parser->getMentionType($microformats); //throw error here?
$type = $parser->getMentionType($microformats); // throw error here?
dispatch(new SaveProfileImage($microformats));
$webmention->source = $this->source;
$webmention->target = $this->note->longurl;
@ -96,21 +98,23 @@ class ProcessWebMention implements ShouldQueue
}
/**
* Retreive the remote content from a URL, and caches the result.
* Save the HTML of a webmention for future use.
*
* @param string $html
* @param string $url
* @param GuzzleHttp\client $guzzle
* @return string|null
*/
private function getRemoteContent($url, Client $guzzle)
private function saveRemoteContent($html, $url)
{
try {
$response = $guzzle->request('GET', $url);
} catch (RequestException $e) {
return;
$filenameFromURL = str_replace(
['https://', 'http://'],
['https/', 'http/'],
$url
);
if (substr($url, -1) == '/') {
$filenameFromURL .= 'index.html';
}
$html = (string) $response->getBody();
$path = storage_path() . '/HTML/' . $this->createFilenameFromURL($url);
$path = storage_path() . '/HTML/' . $filenameFromURL;
$parts = explode('/', $path);
$name = array_pop($parts);
$dir = implode('/', $parts);
@ -118,24 +122,5 @@ class ProcessWebMention implements ShouldQueue
mkdir($dir, 0755, true);
}
file_put_contents("$dir/$name", $html);
return $html;
}
/**
* Create a file path from a URL. This is used when caching the HTML
* response.
*
* @param string The URL
* @return string The path name
*/
private function createFilenameFromURL($url)
{
$url = str_replace(['https://', 'http://'], ['https/', 'http/'], $url);
if (substr($url, -1) == '/') {
$url = $url . 'index.html';
}
return $url;
}
}

View file

@ -44,7 +44,7 @@ class SaveProfileImage implements ShouldQueue
//dont save pbs.twimg.com links
if (parse_url($photo, PHP_URL_HOST) != 'pbs.twimg.com'
&& parse_url($photo, PHP_URL_HOST) != 'twitter.com') {
$client = new Client();
$client = resolve(Client::class);
try {
$response = $client->get($photo);
$image = $response->getBody(true);

View file

@ -2,7 +2,7 @@
namespace App\Jobs;
use App\Note;
use App\Models\Note;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
@ -29,18 +29,18 @@ class SendWebMentions implements ShouldQueue
/**
* Execute the job.
*
* @param \GuzzleHttp\Client $guzzle
* @return void
*/
public function handle(Client $guzzle)
public function handle()
{
//grab the URLs
$urlsInReplyTo = explode(' ', $this->note->in_reply_to);
$urlsNote = $this->getLinks($this->note->note);
$urls = array_filter(array_merge($urlsInReplyTo, $urlsNote)); //filter out none URLs
foreach ($urls as $url) {
$endpoint = $this->discoverWebmentionEndpoint($url, $guzzle);
if ($endpoint) {
$endpoint = $this->discoverWebmentionEndpoint($url);
if ($endpoint !== null) {
$guzzle = resolve(Client::class);
$guzzle->post($endpoint, [
'form_params' => [
'source' => $this->note->longurl,
@ -55,21 +55,21 @@ class SendWebMentions implements ShouldQueue
* Discover if a URL has a webmention endpoint.
*
* @param string The URL
* @param \GuzzleHttp\Client $guzzle
* @return string The webmention endpoint URL
*/
public function discoverWebmentionEndpoint($url, $guzzle)
public function discoverWebmentionEndpoint($url)
{
//lets not send webmentions to myself
if (parse_url($url, PHP_URL_HOST) == config('app.longurl')) {
return false;
return;
}
if (starts_with($url, '/notes/tagged/')) {
return false;
return;
}
$endpoint = null;
$guzzle = resolve(Client::class);
$response = $guzzle->get($url);
//check HTTP Headers for webmention endpoint
$links = \GuzzleHttp\Psr7\parse_header($response->getHeader('Link'));
@ -92,8 +92,6 @@ class SendWebMentions implements ShouldQueue
if ($endpoint) {
return $this->resolveUri($endpoint, $url);
}
return false;
}
/**

View file

@ -2,16 +2,17 @@
namespace App\Jobs;
use App\Bookmark;
use GuzzleHttp\Client;
use App\Models\Bookmark;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class SyndicateBookmarkToFacebook implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $bookmark;

View file

@ -2,16 +2,17 @@
namespace App\Jobs;
use App\Bookmark;
use GuzzleHttp\Client;
use App\Models\Bookmark;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class SyndicateBookmarkToTwitter implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $bookmark;

View file

@ -2,7 +2,7 @@
namespace App\Jobs;
use App\Note;
use App\Models\Note;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;

View file

@ -2,7 +2,7 @@
namespace App\Jobs;
use App\Note;
use App\Models\Note;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Cviebrock\EloquentSluggable\Sluggable;
@ -17,7 +17,7 @@ class Article extends Model
*
* @var array
*/
protected $dates = ['deleted_at'];
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
/**
* The database table used by the model.
@ -40,16 +40,6 @@ class Article extends Model
];
}
/**
* Define the relationship with webmentions.
*
* @var array
*/
public function webmentions()
{
return $this->morphMany('App\WebMention', 'commentable');
}
/**
* We shall set a blacklist of non-modifiable model attributes.
*
@ -66,7 +56,7 @@ class Article extends Model
{
$markdown = new CommonMarkConverter();
$html = $markdown->convertToHtml($this->main);
//change <pre><code>[lang] ~> <pre><code data-language="lang">
// changes <pre><code>[lang] ~> <pre><code data-language="lang">
$match = '/<pre><code>\[(.*)\]\n/';
$replace = '<pre><code class="language-$1">';
$text = preg_replace($match, $replace, $html);
@ -130,20 +120,20 @@ class Article extends Model
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeDate($query, $year = null, $month = null)
public function scopeDate($query, int $year = null, int $month = null)
{
if ($year == null) {
return $query;
}
$start = $year . '-01-01 00:00:00';
$end = ($year + 1) . '-01-01 00:00:00';
if (($month !== null) && ($month !== '12')) {
if (($month !== null) && ($month !== 12)) {
$start = $year . '-' . $month . '-01 00:00:00';
$end = $year . '-' . ($month + 1) . '-01 00:00:00';
}
if ($month === '12') {
if ($month === 12) {
$start = $year . '-12-01 00:00:00';
//$end as above
$end = ($year + 1) . '-01-01 00:00:00';
}
return $query->where([

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
@ -27,7 +27,7 @@ class Bookmark extends Model
*/
public function tags()
{
return $this->belongsToMany('App\Tag');
return $this->belongsToMany('App\Models\Tag');
}
/**

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Mf2;
use HTMLPurifier;
@ -24,7 +24,7 @@ class Like extends Model
public function getContentAttribute($value)
{
if ($value === null) {
return $this->url;
return null;
}
$mf2 = Mf2\parse($value, $this->url);

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
@ -18,14 +18,14 @@ class Media extends Model
*
* @var array
*/
protected $fillable = ['path'];
protected $fillable = ['token', 'path', 'type', 'image_widths'];
/**
* Get the note that owns this media.
*/
public function note()
{
return $this->belongsTo('App\Note');
return $this->belongsTo('App\Models\Note');
}
/**
@ -70,10 +70,10 @@ class Media extends Model
public function getBasename($path)
{
$filenameParts = explode('.', $path);
// the following achieves this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
$filenameParts = explode('.', $path);
array_pop($filenameParts);
$basename = ltrim(array_reduce($filenameParts, function ($carry, $item) {
return $carry . '.' . $item;
}, ''), '.');

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
@ -27,6 +27,6 @@ class MicropubClient extends Model
*/
public function notes()
{
return $this->hasMany('App\Note', 'client_id', 'client_url');
return $this->hasMany('App\Models\Note', 'client_id', 'client_url');
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Cache;
use Twitter;
@ -69,7 +69,7 @@ class Note extends Model
*/
public function tags()
{
return $this->belongsToMany('App\Tag');
return $this->belongsToMany('App\Models\Tag');
}
/**
@ -79,7 +79,7 @@ class Note extends Model
*/
public function client()
{
return $this->belongsTo('App\MicropubClient', 'client_id', 'client_url');
return $this->belongsTo('App\Models\MicropubClient', 'client_id', 'client_url');
}
/**
@ -89,7 +89,7 @@ class Note extends Model
*/
public function webmentions()
{
return $this->morphMany('App\WebMention', 'commentable');
return $this->morphMany('App\Models\WebMention', 'commentable');
}
/**
@ -99,7 +99,7 @@ class Note extends Model
*/
public function place()
{
return $this->belongsTo('App\Place');
return $this->belongsTo('App\Models\Place');
}
/**
@ -109,7 +109,7 @@ class Note extends Model
*/
public function media()
{
return $this->hasMany('App\Media');
return $this->hasMany('App\Models\Media');
}
/**
@ -146,9 +146,9 @@ class Note extends Model
$emoji = new EmojiModifier();
$hcards = $this->makeHCards($value);
$html = $this->convertMarkdown($hcards);
$hashtags = $this->autoLinkHashtag($html);
$modified = $emoji->makeEmojiAccessible($hashtags);
$hashtags = $this->autoLinkHashtag($hcards);
$html = $this->convertMarkdown($hashtags);
$modified = $emoji->makeEmojiAccessible($html);
return $modified;
}
@ -223,9 +223,7 @@ class Note extends Model
public function getLatitudeAttribute()
{
if ($this->place !== null) {
$lnglat = explode(' ', $this->place->location);
return $lnglat[1];
return $this->place->location->getLat();
}
if ($this->location !== null) {
$pieces = explode(':', $this->location);
@ -243,9 +241,7 @@ class Note extends Model
public function getLongitudeAttribute()
{
if ($this->place !== null) {
$lnglat = explode(' ', $this->place->location);
return $lnglat[1];
return $this->place->location->getLng();
}
if ($this->location !== null) {
$pieces = explode(':', $this->location);
@ -281,12 +277,13 @@ class Note extends Model
if (Cache::has($tweetId)) {
return Cache::get($tweetId);
}
try {
$oEmbed = Twitter::getOembed([
'id' => $tweetId,
'url' => $this->in_reply_to,
'dnt' => true,
'align' => 'center',
'maxwidth' => 550,
'maxwidth' => 512,
]);
} catch (\Exception $e) {
return;
@ -408,10 +405,10 @@ class Note extends Model
$contact = $this->contacts[$matches[1]]; // easier to read the following code
$host = parse_url($contact->homepage, PHP_URL_HOST);
$contact->photo = (file_exists(public_path() . '/assets/profile-images/' . $host . '/image')) ?
'/assets/profile-images/' . $host . '/image'
:
'/assets/profile-images/default-image';
$contact->photo = '/assets/profile-images/default-image';
if (file_exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
$contact->photo = '/assets/profile-images/' . $host . '/image';
}
return trim(view('templates.mini-hcard', ['contact' => $contact])->render());
},
@ -450,31 +447,17 @@ class Note extends Model
* @param string The note
* @return string
*/
private function autoLinkHashtag($text)
public function autoLinkHashtag($text)
{
// $replacements = ["#tag" => "<a rel="tag" href="/tags/tag">#tag</a>]
$replacements = [];
$matches = [];
if (preg_match_all('/(?<=^|\s)\#([a-zA-Z0-9\-\_]+)/i', $text, $matches, PREG_PATTERN_ORDER)) {
// Look up #tags, get Full name and URL
foreach ($matches[0] as $name) {
$name = str_replace('#', '', $name);
$replacements[$name] =
'<a rel="tag" class="p-category" href="/notes/tagged/'
. Tag::normalize($name)
. '">#'
. $name
. '</a>';
}
// Replace #tags with valid microformat-enabled link
foreach ($replacements as $name => $replacement) {
$text = str_replace('#' . $name, $replacement, $text);
}
}
return $text;
return preg_replace_callback(
'/#([^\s]*)\b/',
function ($matches) {
return '<a rel="tag" class="p-category" href="/notes/tagged/'
. Tag::normalize($matches[1]) . '">#'
. Tag::normalize($matches[1]) . '</a>';
},
$text
);
}
private function convertMarkdown($text)
@ -536,7 +519,7 @@ class Note extends Model
return $address;
}
$adress = '<span class="p-country-name">' . $json->address->country . '</span>';
$address = '<span class="p-country-name">' . $json->address->country . '</span>';
Cache::forever($latlng, $address);
return $address;

View file

@ -1,8 +1,8 @@
<?php
namespace App;
namespace App\Models;
use DB;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Cviebrock\EloquentSluggable\Sluggable;
@ -53,7 +53,7 @@ class Place extends Model
*/
public function notes()
{
return $this->hasMany('App\Note');
return $this->hasMany('App\Models\Note');
}
/**
@ -79,15 +79,8 @@ class Place extends Model
public function scopeWhereExternalURL(Builder $query, string $url)
{
$type = $this->getType($url);
if ($type === null) {
// we havent set a type, therefore result must be empty set
// id cant be null, so this will return empty set
return $query->whereNull('id');
}
return $query->where('external_urls', '@>', json_encode([
$type => $url,
$this->getType($url) => $url,
]));
}
@ -131,12 +124,22 @@ class Place extends Model
return config('app.shorturl') . '/places/' . $this->slug;
}
/**
* This method is an alternative for `longurl`.
*
* @return string
*/
public function getUriAttribute()
{
return $this->longurl;
}
public function setExternalUrlsAttribute($url)
{
$type = $this->getType($url);
if ($type === null) {
throw new \Exception('Unkown external url type ' . $url);
if ($url === null) {
return;
}
$type = $this->getType($url);
$already = [];
if (array_key_exists('external_urls', $this->attributes)) {
$already = json_decode($this->attributes['external_urls'], true);
@ -155,6 +158,6 @@ class Place extends Model
return 'osm';
}
return null;
return 'default';
}
}

View file

@ -1,11 +1,18 @@
<?php
namespace App;
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
/**
* We shall set a blacklist of non-modifiable model attributes.
*
* @var array
*/
protected $guarded = ['id'];
/**
* Define the relationship with tags.
*
@ -13,7 +20,7 @@ class Tag extends Model
*/
public function notes()
{
return $this->belongsToMany('App\Note');
return $this->belongsToMany('App\Models\Note');
}
/**
@ -21,16 +28,9 @@ class Tag extends Model
*/
public function bookmarks()
{
return $this->belongsToMany('App\Bookmark');
return $this->belongsToMany('App\Models\Bookmark');
}
/**
* We shall set a blacklist of non-modifiable model attributes.
*
* @var array
*/
protected $guarded = ['id'];
/**
* Normalize tags so theyre lowercase and fancy diatrics are removed.
*

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

View file

@ -1,6 +1,6 @@
<?php
namespace App;
namespace App\Models;
use Cache;
use Twitter;
@ -19,6 +19,13 @@ class WebMention extends Model
*/
protected $table = 'webmentions';
/**
* We shall set a blacklist of non-modifiable model attributes.
*
* @var array
*/
protected $guarded = ['id'];
/**
* Define the relationship.
*
@ -29,13 +36,6 @@ class WebMention extends Model
return $this->morphTo();
}
/**
* We shall set a blacklist of non-modifiable model attributes.
*
* @var array
*/
protected $guarded = ['id'];
/**
* Get the author of the webmention.
*
@ -78,9 +78,9 @@ class WebMention extends Model
}
/**
* Get the filteres HTML of a reply.
* Get the filtered HTML of a reply.
*
* @return strin|null
* @return string|null
*/
public function getReplyAttribute()
{
@ -108,14 +108,10 @@ class WebMention extends Model
if (Cache::has($url)) {
return Cache::get($url);
}
$username = parse_url($url, PHP_URL_PATH);
try {
$info = Twitter::getUsers(['screen_name' => $username]);
$profile_image = $info->profile_image_url_https;
Cache::put($url, $profile_image, 10080); //1 week
} catch (Exception $e) {
return $url; //not sure here
}
$username = ltrim(parse_url($url, PHP_URL_PATH), '/');
$info = Twitter::getUsers(['screen_name' => $username]);
$profile_image = $info->profile_image_url_https;
Cache::put($url, $profile_image, 10080); //1 week
return $profile_image;
}

View file

@ -2,7 +2,7 @@
namespace App\Observers;
use App\{Note, Tag};
use App\Models\{Note, Tag};
class NoteObserver
{
@ -21,12 +21,10 @@ class NoteObserver
}
$tags->transform(function ($tag) {
return Tag::firstOrCreate(['tag' => $tag]);
});
return Tag::firstOrCreate(['tag' => $tag])->id;
})->toArray();
$note->tags()->attach($tags->map(function ($tag) {
return $tag->id;
}));
$note->tags()->attach($tags);
}
/**
@ -65,9 +63,6 @@ class NoteObserver
public function getTagsFromNote($note)
{
preg_match_all('/#([^\s<>]+)\b/', $note, $tags);
if (array_get($tags, '1') === null) {
return [];
}
return collect($tags[1])->map(function ($tag) {
return Tag::normalize($tag);

View file

@ -2,7 +2,7 @@
namespace App\Providers;
use App\Note;
use App\Models\Note;
use Illuminate\Http\Request;
use App\Observers\NoteObserver;
use Laravel\Dusk\DuskServiceProvider;

View file

@ -5,6 +5,9 @@ namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
/**
* @codeCoverageIgnore
*/
class BroadcastServiceProvider extends ServiceProvider
{
/**

View file

@ -6,6 +6,9 @@ use Illuminate\Http\Request;
use Laravel\Horizon\Horizon;
use Illuminate\Support\ServiceProvider;
/**
* @codeCoverageIgnore
*/
class HorizonServiceProvider extends ServiceProvider
{
/**

View file

@ -2,7 +2,7 @@
namespace App\Services;
use App\Note;
use App\Models\Note;
class ActivityStreamsService
{

View file

@ -4,42 +4,39 @@ declare(strict_types=1);
namespace App\Services;
use App\Tag;
use App\Bookmark;
use Ramsey\Uuid\Uuid;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use App\Jobs\ProcessBookmark;
use App\Models\{Bookmark, Tag};
use Spatie\Browsershot\Browsershot;
use App\Jobs\SyndicateBookmarkToTwitter;
use App\Jobs\SyndicateBookmarkToFacebook;
use App\Exceptions\InternetArchiveErrorSavingException;
use GuzzleHttp\Exception\ClientException;
use App\Exceptions\InternetArchiveException;
class BookmarkService
{
/**
* Create a new Bookmark.
*
* @param Request $request
* @param array $request
* @return Bookmark $bookmark
*/
public function createBookmark(Request $request): Bookmark
public function createBookmark(array $request): Bookmark
{
if ($request->header('Content-Type') == 'application/json') {
if (array_get($request, 'properties.bookmark-of.0')) {
//micropub request
$url = normalize_url($request->input('properties.bookmark-of.0'));
$name = $request->input('properties.name.0');
$content = $request->input('properties.content.0');
$categories = $request->input('properties.category');
$url = normalize_url(array_get($request, 'properties.bookmark-of.0'));
$name = array_get($request, 'properties.name.0');
$content = array_get($request, 'properties.content.0');
$categories = array_get($request, 'properties.category');
}
if (
($request->header('Content-Type') == 'application/x-www-form-urlencoded')
||
(str_contains($request->header('Content-Type'), 'multipart/form-data'))
) {
$url = normalize_url($request->input('bookmark-of'));
$name = $request->input('name');
$content = $request->input('content');
$categories = $request->input('category');
if (array_get($request, 'bookmark-of')) {
$url = normalize_url(array_get($request, 'bookmark-of'));
$name = array_get($request, 'name');
$content = array_get($request, 'content');
$categories = array_get($request, 'category');
}
$bookmark = Bookmark::create([
@ -55,11 +52,11 @@ class BookmarkService
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
$mpSyndicateTo = null;
if ($request->has('mp-syndicate-to')) {
$mpSyndicateTo = $request->input('mp-syndicate-to');
if (array_get($request, 'mp-syndicate-to')) {
$mpSyndicateTo = array_get($request, 'mp-syndicate-to');
}
if ($request->has('properties.mp-syndicate-to')) {
$mpSyndicateTo = $request->input('properties.mp-syndicate-to');
if (array_get($request, 'properties.mp-syndicate-to')) {
$mpSyndicateTo = array_get($request, 'properties.mp-syndicate-to');
}
if (is_string($mpSyndicateTo)) {
$service = array_search($mpSyndicateTo, $targets);
@ -104,20 +101,20 @@ class BookmarkService
public function getArchiveLink(string $url): string
{
$client = new Client();
$response = $client->request('GET', 'https://web.archive.org/save/' . $url);
$client = resolve(Client::class);
try {
$response = $client->request('GET', 'https://web.archive.org/save/' . $url);
} catch (ClientException $e) {
//throw an exception to be caught
throw new InternetArchiveException;
}
if ($response->hasHeader('Content-Location')) {
if (starts_with($response->getHeader('Content-Location')[0], '/web')) {
if (starts_with(array_get($response->getHeader('Content-Location'), 0), '/web')) {
return $response->getHeader('Content-Location')[0];
}
}
if (starts_with(array_get($response->getHeader('Content-Location'), 0), '/web')) {
return $response->getHeader('Content-Location')[0];
}
//throw an exception to be caught
throw new InternetArchiveErrorSavingException;
throw new InternetArchiveException;
}
}

View file

@ -4,29 +4,25 @@ declare(strict_types=1);
namespace App\Services;
use App\Like;
use App\Models\Like;
use App\Jobs\ProcessLike;
use Illuminate\Http\Request;
class LikeService
{
/**
* Create a new Like.
*
* @param Request $request
* @param array $request
* @return Like $like
*/
public function createLike(Request $request): Like
public function createLike(array $request): Like
{
if ($request->header('Content-Type') == 'application/json') {
if (array_get($request, 'properties.like-of.0')) {
//micropub request
$url = normalize_url($request->input('properties.like-of.0'));
$url = normalize_url(array_get($request, 'properties.like-of.0'));
}
if (
($request->header('Content-Type') == 'x-www-url-formencoded')
||
($request->header('Content-Type') == 'multipart/form-data')
) {
$url = normalize_url($request->input('like-of'));
if (array_get($request, 'like-of')) {
$url = normalize_url(array_get($request, 'like-of'));
}
$like = Like::create(['url' => $url]);

View file

@ -0,0 +1,27 @@
<?php
namespace App\Services\Micropub;
use App\Services\PlaceService;
class HCardService
{
public function process(array $request)
{
$data = [];
if (array_get($request, 'properties.name')) {
$data['name'] = array_get($request, 'properties.name');
$data['description'] = array_get($request, 'properties.description');
$data['geo'] = array_get($request, 'properties.geo');
} else {
$data['name'] = array_get($request, 'name');
$data['description'] = array_get($request, 'description');
$data['geo'] = array_get($request, 'geo');
$data['latitude'] = array_get($request, 'latitude');
$data['longitude'] = array_get($request, 'longitude');
}
$place = resolve(PlaceService::class)->createPlace($data);
return $place->longurl;
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Services\Micropub;
use App\Services\{BookmarkService, LikeService, NoteService};
class HEntryService
{
public function process(array $request, string $client = null)
{
if (array_get($request, 'properties.like-of') || array_get($request, 'like-of')) {
$like = resolve(LikeService::class)->createLike($request);
return $like->longurl;
}
if (array_get($request, 'properties.bookmark-of') || array_get($request, 'bookmark-of')) {
$bookmark = resolve(BookmarkService::class)->createBookmark($request);
return $bookmark->longurl;
}
$note = resolve(NoteService::class)->createNote($request, $client);
return $note->longurl;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace App\Services\Micropub;
use App\Models\{Media, Note};
use Illuminate\Database\Eloquent\ModelNotFoundException;
class UpdateService
{
public function process(array $request)
{
$urlPath = parse_url(array_get($request, 'url'), PHP_URL_PATH);
//is it a note we are updating?
if (mb_substr($urlPath, 1, 5) !== 'notes') {
return response()->json([
'error' => 'invalid',
'error_description' => 'This implementation currently only support the updating of notes',
], 500);
}
try {
$note = Note::nb60(basename($urlPath))->firstOrFail();
} catch (ModelNotFoundException $exception) {
return response()->json([
'error' => 'invalid_request',
'error_description' => 'No known note with given ID',
], 404);
}
//got the note, are we dealing with a “replace” request?
if (array_get($request, 'replace')) {
foreach (array_get($request, 'replace') as $property => $value) {
if ($property == 'content') {
$note->note = $value[0];
}
if ($property == 'syndication') {
foreach ($value as $syndicationURL) {
if (starts_with($syndicationURL, 'https://www.facebook.com')) {
$note->facebook_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://www.swarmapp.com')) {
$note->swarm_url = $syndicationURL;
}
if (starts_with($syndicationURL, 'https://twitter.com')) {
$note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH));
}
}
}
}
$note->save();
return response()->json([
'response' => 'updated',
]);
}
//how about “add”
if (array_get($request, 'add')) {
foreach (array_get($request, '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 (starts_with($photoURL, '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_description' => 'unsupported request',
], 500);
}
}

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Services;
use App\{Media, Note, Place};
use App\Models\{Media, Note, Place};
use App\Jobs\{SendWebMentions, SyndicateNoteToFacebook, SyndicateNoteToTwitter};
class NoteService
@ -12,91 +12,38 @@ class NoteService
/**
* Create a new note.
*
* @param array $data
* @param array $request
* @param string $client
* @return \App\Note $note
*/
public function createNote(array $data): Note
public function createNote(array $request, string $client = null): Note
{
//check the input
if (array_key_exists('content', $data) === false) {
$data['content'] = null;
}
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'],
'in_reply_to' => $data['in-reply-to'],
'client_id' => $data['client-id'],
'note' => $this->getContent($request),
'in_reply_to' => $this->getInReplyTo($request),
'client_id' => $client,
]
);
if (array_key_exists('published', $data) && empty($data['published']) === false) {
$carbon = carbon($data['published']);
$note->created_at = $note->updated_at = $carbon->toDateTimeString();
if ($this->getPublished($request)) {
$note->created_at = $note->updated_at = $this->getPublished($request);
}
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
//get the URL path, then take last part, we can hack with basename
//as path looks like file path.
$place = Place::where('slug', basename(parse_url($data['location'], PHP_URL_PATH)))->first();
$note->place()->associate($place);
}
if (substr($data['location'], 0, 4) == 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$data['location'],
$matches
);
$note->location = $matches[0][0] . ', ' . $matches[0][1];
$note->location = $this->getLocation($request);
if ($this->getCheckin($request)) {
$note->place()->associate($this->getCheckin($request));
$note->swarm_url = $this->getSwarmUrl($request);
if ($note->note === null || $note->note == '') {
$note->note = 'Ive just checked in with Swarm';
}
}
if (array_key_exists('checkin', $data) && $data['checkin'] !== null) {
$place = Place::where('slug', basename(parse_url($data['checkin'], PHP_URL_PATH)))->first();
if ($place !== null) {
$note->place()->associate($place);
$note->swarm_url = $data['swarm-url'];
if ($note->note === null || $note->note == '') {
$note->note = 'Ive just checked in with Swarm';
}
}
}
$note->instagram_url = $this->getInstagramUrl($request);
/* drop image support for now
//add images to media library
if ($request->hasFile('photo')) {
$files = $request->file('photo');
foreach ($files as $file) {
$note->addMedia($file)->toCollectionOnDisk('images', 's3');
}
}
*/
//add support for media uploaded as URLs
if (array_key_exists('photo', $data)) {
foreach ($data['photo'] as $photo) {
// 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();
} else {
$media = Media::firstOrNew(['path' => $photo]);
// currently assuming this is a photo from Swarm or OwnYourGram
$media->type = 'image';
$media->save();
}
$note->media()->save($media);
}
if (array_key_exists('instagram-url', $data)) {
$note->instagram_url = $data['instagram-url'];
}
foreach ($this->getMedia($request) as $media) {
$note->media()->save($media);
}
$note->save();
@ -104,13 +51,175 @@ class NoteService
dispatch(new SendWebMentions($note));
//syndication targets
if (in_array('twitter', $data['syndicate'])) {
dispatch(new SyndicateNoteToTwitter($note));
}
if (in_array('facebook', $data['syndicate'])) {
dispatch(new SyndicateNoteToFacebook($note));
if (count($this->getSyndicationTargets($request)) > 0) {
if (in_array('twitter', $this->getSyndicationTargets($request))) {
dispatch(new SyndicateNoteToTwitter($note));
}
if (in_array('facebook', $this->getSyndicationTargets($request))) {
dispatch(new SyndicateNoteToFacebook($note));
}
}
return $note;
}
private function getContent(array $request): ?string
{
if (array_get($request, 'properties.content.0.html')) {
return array_get($request, 'properties.content.0.html');
}
if (is_string(array_get($request, 'properties.content.0'))) {
return array_get($request, 'properties.content.0');
}
return array_get($request, 'content');
}
private function getInReplyTo(array $request): ?string
{
if (array_get($request, 'properties.in-reply-to.0')) {
return array_get($request, 'properties.in-reply-to.0');
}
return array_get($request, 'in-reply-to');
}
private function getPublished(array $request): ?string
{
if (array_get($request, 'properties.published.0')) {
return carbon(array_get($request, 'properties.published.0'))
->toDateTimeString();
}
if (array_get($request, 'published')) {
return carbon(array_get($request, 'published'))->toDateTimeString();
}
return null;
}
private function getLocation(array $request): ?string
{
$location = array_get($request, 'properties.location.0') ?? array_get($request, 'location');
if (is_string($location) && substr($location, 0, 4) == 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$location,
$matches
);
return $matches[0][0] . ', ' . $matches[0][1];
}
return null;
}
private function getCheckin(array $request): ?Place
{
if (array_get($request, 'properties.location.0.type.0') === 'h-card') {
try {
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
array_get($request, 'properties.location.0')
);
} catch (\InvalidArgumentException $e) {
return null;
}
return $place;
}
if (starts_with(array_get($request, 'properties.location.0'), config('app.url'))) {
return Place::where(
'slug',
basename(
parse_url(
array_get($request, 'properties.location.0'),
PHP_URL_PATH
)
)
)->first();
}
if (array_get($request, 'properties.checkin')) {
try {
$place = resolve(PlaceService::class)->createPlaceFromCheckin(
array_get($request, 'properties.checkin.0')
);
} catch (\InvalidArgumentException $e) {
return null;
}
return $place;
}
return null;
}
private function getSwarmUrl(array $request): ?string
{
if (stristr(array_get($request, 'properties.syndication.0', ''), 'swarmapp')) {
return array_get($request, 'properties.syndication.0');
}
return null;
}
private function getSyndicationTargets(array $request): array
{
$syndication = [];
$targets = array_pluck(config('syndication.targets'), 'uid', 'service.name');
$mpSyndicateTo = array_get($request, 'mp-syndicate-to') ?? array_get($request, 'properties.mp-syndicate-to');
if (is_string($mpSyndicateTo)) {
$service = array_search($mpSyndicateTo, $targets);
if ($service == 'Twitter') {
$syndication[] = 'twitter';
}
if ($service == 'Facebook') {
$syndication[] = 'facebook';
}
}
if (is_array($mpSyndicateTo)) {
foreach ($mpSyndicateTo as $uid) {
$service = array_search($uid, $targets);
if ($service == 'Twitter') {
$syndication[] = 'twitter';
}
if ($service == 'Facebook') {
$syndication[] = 'facebook';
}
}
}
return $syndication;
}
private function getMedia(array $request): array
{
$media = [];
$photos = array_get($request, 'photo') ?? array_get($request, 'properties.photo');
if (isset($photos)) {
foreach ((array) $photos as $photo) {
// 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();
} else {
$newMedia = Media::firstOrNew(['path' => $photo]);
// currently assuming this is a photo from Swarm or OwnYourGram
$newMedia->type = 'image';
$newMedia->save();
$media[] = $newMedia;
}
}
}
return $media;
}
private function getInstagramUrl(array $request): ?string
{
if (starts_with(array_get($request, 'properties.syndication.0'), 'https://www.instagram.com')) {
return array_get($request, 'properties.syndication.0');
}
return null;
}
}

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Services;
use App\Place;
use App\Models\Place;
use Phaza\LaravelPostgis\Geometries\Point;
class PlaceService
@ -19,7 +19,7 @@ class PlaceService
{
//obviously a place needs a lat/lng, but this could be sent in a geo-url
//if no geo array key, we assume the array already has lat/lng values
if (array_key_exists('geo', $data)) {
if (array_key_exists('geo', $data) && $data['geo'] !== null) {
preg_match_all(
'/([0-9\.\-]+)/',
$data['geo'],
@ -46,24 +46,24 @@ class PlaceService
public function createPlaceFromCheckin(array $checkin): Place
{
//check if the place exists if from swarm
if (array_key_exists('url', $checkin['properties'])) {
$place = Place::whereExternalURL($checkin['properties']['url'][0])->get();
if (array_has($checkin, 'properties.url')) {
$place = Place::whereExternalURL(array_get($checkin, 'properties.url.0'))->get();
if (count($place) === 1) {
return $place->first();
}
}
if (array_key_exists('name', $checkin['properties']) === false) {
if (array_has($checkin, 'properties.name') === false) {
throw new \InvalidArgumentException('Missing required name');
}
if (array_key_exists('latitude', $checkin['properties']) === false) {
if (array_has($checkin, 'properties.latitude') === false) {
throw new \InvalidArgumentException('Missing required longitude/latitude');
}
$place = new Place();
$place->name = $checkin['properties']['name'][0];
$place->external_urls = $checkin['properties']['url'][0];
$place->name = array_get($checkin, 'properties.name.0');
$place->external_urls = array_get($checkin, 'properties.url.0');
$place->location = new Point(
(float) $checkin['properties']['latitude'][0],
(float) $checkin['properties']['longitude'][0]
(float) array_get($checkin, 'properties.latitude.0'),
(float) array_get($checkin, 'properties.longitude.0')
);
$place->save();

View file

@ -38,7 +38,7 @@ class TokenService
* @param string The token
* @return mixed
*/
public function validateToken(string $bearerToken): ?Token
public function validateToken(string $bearerToken): Token
{
$signer = new Sha256();
try {
@ -47,7 +47,7 @@ class TokenService
throw new InvalidTokenException('Token could not be parsed');
}
if (! $token->verify($signer, config('app.key'))) {
throw new InvalidTokenException('Token failed verification');
throw new InvalidTokenException('Token failed validation');
}
return $token;

View file

@ -1,5 +1,10 @@
# Changelog
## Version 0.14 (2017-12-22)
- Tests
- Refactor
- More tests, seriously, code-coverage to now above 90%
## Version 0.13.1 (2017-11-20)
- A small fix when adding a new bookmark

View file

@ -35,12 +35,14 @@
"require-dev": {
"barryvdh/laravel-debugbar": "~3.0",
"bmitch/churn-php": "^0.2.0",
"codedungeon/phpunit-result-printer": "^0.3.0",
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"jakub-onderka/php-parallel-lint": "^0.9.2",
"laravel/dusk": "^2.0",
"mockery/mockery": "0.9.*",
"nunomaduro/collision": "^1.1",
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "~6.0",
"sebastian/phpcpd": "^3.0"
},

1052
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
<?php
use App\Models\Bookmark;
use Faker\Generator as Faker;
$factory->define(App\Bookmark::class, function (Faker $faker) {
$factory->define(Bookmark::class, function (Faker $faker) {
return [
'url' => $faker->url,
'name' => $faker->sentence,

View file

@ -1,8 +1,9 @@
<?php
use App\Models\Like;
use Faker\Generator as Faker;
$factory->define(App\Like::class, function (Faker $faker) {
$factory->define(Like::class, function (Faker $faker) {
return [
'url' => $faker->url,
'author_name' => $faker->name,

View file

@ -1,8 +1,9 @@
<?php
use App\Models\Note;
use Faker\Generator as Faker;
$factory->define(App\Note::class, function (Faker $faker) {
$factory->define(Note::class, function (Faker $faker) {
return [
'note' => $faker->paragraph,
];

View file

@ -1,8 +1,9 @@
<?php
use App\Models\Tag;
use Faker\Generator as Faker;
$factory->define(App\Tag::class, function (Faker $faker) {
$factory->define(Tag::class, function (Faker $faker) {
return [
'tag' => $faker->word,
];

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdateModelsReferenceInWebmentionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('webmentions', function (Blueprint $table) {
DB::statement("UPDATE webmentions SET commentable_type = 'App\\Models\\Note' WHERE commentable_type = 'App\\Note'");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('webmentions', function (Blueprint $table) {
DB::statement("UPDATE webmentions SET commentable_type = 'App\\Note' WHERE commentable_type = 'App\\Models\\Note'");
});
}
}

View file

@ -1,6 +1,6 @@
<?php
use App\Article;
use App\Models\Article;
use Illuminate\Database\Seeder;
class ArticlesTableSeeder extends Seeder

View file

@ -1,5 +1,6 @@
<?php
use App\Models\{Bookmark, Tag};
use Illuminate\Database\Seeder;
class BookmarksTableSeeder extends Seeder
@ -11,8 +12,8 @@ class BookmarksTableSeeder extends Seeder
*/
public function run()
{
factory(App\Bookmark::class, 10)->create()->each(function ($bookmark) {
$bookmark->tags()->save(factory(App\Tag::class)->make());
factory(Bookmark::class, 10)->create()->each(function ($bookmark) {
$bookmark->tags()->save(factory(Tag::class)->make());
});
}
}

View file

@ -1,6 +1,6 @@
<?php
use App\Contact;
use App\Models\Contact;
use Illuminate\Database\Seeder;
class ContactsTableSeeder extends Seeder
@ -22,7 +22,6 @@ class ContactsTableSeeder extends Seeder
'nick' => 'aaron',
'name' => 'Aaron Parecki',
'homepage' => 'https://aaronparecki.com',
'twitter' => 'aaronpk',
'facebook' => '123456',
]);
}

View file

@ -1,5 +1,7 @@
<?php
use App\Models\Like;
use Faker\Generator;
use Illuminate\Database\Seeder;
class LikesTableSeeder extends Seeder
@ -11,6 +13,16 @@ class LikesTableSeeder extends Seeder
*/
public function run()
{
factory(App\Like::class, 10)->create();
factory(Like::class, 10)->create();
$faker = new Generator();
$faker->addProvider(new \Faker\Provider\en_US\Person($faker));
$faker->addProvider(new \Faker\Provider\Lorem($faker));
$faker->addProvider(new \Faker\Provider\Internet($faker));
Like::create([
'url' => $faker->url,
'author_url' => $faker->url,
'author_name' => $faker->name,
]);
}
}

View file

@ -1,6 +1,7 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\{Media, Note, Place};
class NotesTableSeeder extends Seeder
{
@ -11,26 +12,31 @@ class NotesTableSeeder extends Seeder
*/
public function run()
{
factory(App\Note::class, 10)->create();
factory(Note::class, 10)->create();
sleep(1);
$noteWithPlace = App\Note::create([
$noteTwitterReply = Note::create([
'note' => 'What does this even mean?',
'in_reply_to' => 'https://twitter.com/realDonaldTrump/status/933662564587855877',
]);
sleep(1);
$noteWithPlace = Note::create([
'note' => 'Having a #beer at the local. 🍺',
]);
$noteWithPlace->tweet_id = '123456789';
$place = App\Place::find(1);
$place = Place::find(1);
$noteWithPlace->place()->associate($place);
$noteWithPlace->save();
sleep(1);
$noteWithContact = App\Note::create([
$noteWithContact = Note::create([
'note' => 'Hi @tantek'
]);
sleep(1);
$noteWithContactPlusPic = App\Note::create([
$noteWithContactPlusPic = Note::create([
'note' => 'Hi @aaron',
'client_id' => 'https://jbl5.dev/notes/new'
]);
sleep(1);
$noteWithoutContact = App\Note::create([
$noteWithoutContact = Note::create([
'note' => 'Hi @bob',
'client_id' => 'https://quill.p3k.io'
]);
@ -41,13 +47,31 @@ class NotesTableSeeder extends Seeder
mkdir(public_path() . '/assets/profile-images/aaronparecki.com', 0755);
copy(base_path() . '/tests/aaron.png', public_path() . '/assets/profile-images/aaronparecki.com/image');
}
$noteWithCoords = App\Note::create([
'note' => 'Note from somehwere',
$noteWithCoords = Note::create([
'note' => 'Note from a town',
]);
$noteWithCoords->location = '53.499,-2.379';
$noteWithCoords->save();
sleep(1);
$noteSyndicated = App\Note::create([
$noteWithCoords2 = Note::create([
'note' => 'Note from a city',
]);
$noteWithCoords2->location = '53.9026894,-2.42250444118781';
$noteWithCoords2->save();
sleep(1);
$noteWithCoords3 = Note::create([
'note' => 'Note from a county',
]);
$noteWithCoords3->location = '57.5066357,-5.0038367';
$noteWithCoords3->save();
sleep(1);
$noteWithCoords4 = Note::create([
'note' => 'Note from a country',
]);
$noteWithCoords4->location = '63.000147,-136.002502';
$noteWithCoords4->save();
sleep(1);
$noteSyndicated = Note::create([
'note' => 'This note has all the syndication targets',
]);
$noteSyndicated->tweet_id = '123456';
@ -56,8 +80,29 @@ class NotesTableSeeder extends Seeder
$noteSyndicated->instagram_url = 'https://www.instagram.com/p/aWsEd123Jh';
$noteSyndicated->save();
sleep(1);
$noteWithTextLinkandEmoji = App\Note::create([
$noteWithTextLinkandEmoji = Note::create([
'note' => 'I love https://duckduckgo.com 💕' // theres a two-heart emoji at the end of this
]);
sleep(1);
$media = new Media();
$media->path = 'media/f1bc8faa-1a8f-45b8-a9b1-57282fa73f87.jpg';
$media->type = 'image';
$media->image_widths = '3648';
$media->save();
$noteWithImage = Note::create([
'note' => 'A lovely waterfall',
]);
$noteWithImage->media()->save($media);
sleep(1);
$noteFromInstagram = Note::create([
'note' => 'Lovely #wedding #weddingfavour',
]);
$noteFromInstagram->instagram_url = 'https://www.instagram.com/p/Bbo22MHhE_0';
$noteFromInstagram->save();
$mediaInstagram = new Media();
$mediaInstagram->path = 'https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/e35/23734479_149605352435937_400133507076063232_n.jpg';
$mediaInstagram->type = 'image';
$mediaInstagram->save();
$noteFromInstagram->media()->save($mediaInstagram);
}
}

View file

@ -1,6 +1,6 @@
<?php
use App\Place;
use App\Models\Place;
use Illuminate\Database\Seeder;
use Phaza\LaravelPostgis\Geometries\Point;

View file

@ -1,6 +1,6 @@
<?php
use App\WebMention;
use App\Models\WebMention;
use Illuminate\Database\Seeder;
class WebMentionsTableSeeder extends Seeder
@ -12,13 +12,21 @@ class WebMentionsTableSeeder extends Seeder
*/
public function run()
{
$webmention = WebMention::create([
'source' => 'https://aaornpk.localhost/reply/1',
'target' => 'https://jonnybarnes.localhost/notes/D',
'commentable_id' => '13',
'commentable_type' => 'App\Note',
$webmentionAaron = WebMention::create([
'source' => 'https://aaronpk.localhost/reply/1',
'target' => config('app.url') . '/notes/E',
'commentable_id' => '14',
'commentable_type' => 'App\Models\Note',
'type' => 'in-reply-to',
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["https://aaronpk.localhost/reply/1"], "name": ["Hi too"], "author": [{"type": ["h-card"], "value": "Aaron Parecki", "properties": {"url": ["https://aaronpk.localhost"], "name": ["Aaron Parecki"], "photo": ["https://aaronparecki.com/images/profile.jpg"]}}], "content": [{"html": "Hi too", "value": "Hi too"}], "published": ["' . date(DATE_W3C) . '"], "in-reply-to": ["https://aaronpk.loclahost/reply/1", "https://jonnybarnes.uk/notes/D"]}}]}'
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["https://aaronpk.localhost/reply/1"], "name": ["Hi too"], "author": [{"type": ["h-card"], "value": "Aaron Parecki", "properties": {"url": ["https://aaronpk.localhost"], "name": ["Aaron Parecki"], "photo": ["https://aaronparecki.com/images/profile.jpg"]}}], "content": [{"html": "Hi too", "value": "Hi too"}], "published": ["' . date(DATE_W3C) . '"], "in-reply-to": ["https://aaronpk.loclahost/reply/1", "' . config('app.url') .'/notes/E"]}}]}'
]);
$webmentionTantek = WebMention::create([
'source' => 'http://tantek.com/',
'target' => config('app.url') . '/notes/D',
'commentable_id' => '13',
'commentable_type' => 'App\Models\Note',
'type' => 'in-reply-to',
'mf2' => '{"rels": [], "items": [{"type": ["h-entry"], "properties": {"url": ["http://tantek.com/"], "name": ["KUTGW"], "author": [{"type": ["h-card"], "value": "Tantek Celik", "properties": {"url": ["http://tantek.com/"], "name": ["Tantek Celik"]}}], "content": [{"html": "kutgw", "value": "kutgw"}], "published": ["' . date(DATE_W3C) . '"], "in-reply-to": ["' . config('app.url') . '/notes/D"]}}]}'
]);
}
}

1471
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,13 +6,13 @@
"license": "CC0-1.0",
"dependencies": {
"alertify.js": "^1.0.12",
"mapbox-gl": "^0.42.0",
"marked": "^0.3.6",
"mapbox-gl": "^0.42.2",
"marked": "^0.3.7",
"normalize.css": "^7.0.0"
},
"devDependencies": {
"ajv": "^5.3.0",
"autoprefixer": "^7.1.6",
"ajv": "^5.5.2",
"autoprefixer": "^7.2.3",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
@ -21,22 +21,23 @@
"babel-preset-latest": "^6.16.0",
"babel-runtime": "^6.26.0",
"dotenv-webpack": "^1.5.4",
"eslint": "^4.11.0",
"eslint": "^4.13.1",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"husky": "^0.14.3",
"lint-staged": "^5.0.0",
"husky": "^0.15.0-beta.16",
"lint-staged": "^6.0.0",
"postcss-cli": "^4.1.1",
"postcss-sass": "^0.2.0",
"pre-commit": "^1.1.3",
"source-list-map": "^2.0.0",
"stylelint": "^8.2.0",
"stylelint-config-standard": "^17.0.0",
"uglify-js": "^3.1.9",
"webpack": "^3.8.1",
"webpack-sources": "^1.0.2"
"stylelint": "^8.4.0",
"stylelint-config-standard": "^18.0.0",
"uglify-js": "^3.2.2",
"webpack": "^3.10.0",
"webpack-sources": "^1.1.0"
},
"scripts": {
"compress": "scripts/compress",
@ -47,17 +48,21 @@
"make:css": "npm run lint:sass && npm run sass && npm run postcss",
"make:js": "npm run lint:es6 && npm run webpack && npm run uglifyjs",
"postcss": "postcss public/assets/css/app.css --use autoprefixer --autoprefixer.browsers \"> 5%\" --replace --map",
"precommit": "lint-staged",
"sass": "sassc --style compressed --sourcemap resources/assets/sass/app.scss public/assets/css/app.css",
"uglifyjs": "scripts/uglifyjs",
"webpack": "webpack --progress --colors"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"resources/assets/es6/*.js": [
"./resources/assets/es6/*.js": [
"eslint --fix",
"git add"
],
"resources/assets/sass/**/*.scss": [
"*.scss": [
"stylelint --syntax=scss --fix",
"git add"
]

8
phpcs.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<ruleset name="jonnybarnes.uk">
<description>Custom configuration for code running jonnybarnes.uk</description>
<file>./app/</file>
<rule ref="PSR2">
<exclude name="PSR2.Namespaces.UseDeclaration" />
</rule>
</ruleset>

View file

@ -7,7 +7,8 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
stopOnFailure="false"
printerClass="Codedungeon\PHPUnitPrettyResultPrinter\Printer">
<testsuites>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View file

@ -1 +1 @@
{"version":3,"sources":["../../../resources/assets/sass/_border-box.scss","../../../resources/assets/sass/_base-font.scss","../../../resources/assets/sass/_header.scss","../../../resources/assets/sass/_variables.scss","../../../resources/assets/sass/_main.scss","../../../resources/assets/sass/_hovercard.scss","../../../resources/assets/sass/_notes.scss","../../../resources/assets/sass/_pagination.scss","../../../resources/assets/sass/_contacts-page.scss","../../../resources/assets/sass/_projects.scss","../../../resources/assets/sass/_footer.scss","../../../resources/assets/sass/_bridgy-links.scss","../../../resources/assets/sass/_emoji.scss","../../../resources/assets/sass/_mapbox.scss","../../../resources/assets/sass/_colors.scss","../../../resources/assets/sass/_styles.scss","../../../resources/assets/sass/_tags.scss"],"names":[],"mappings":"AAKA,KACI,8BAAsB,AAAtB,qBAAsB,CACzB,qBAKG,2BAAmB,AAAnB,kBAAmB,CACtB,KCVG,eACA,yBAA0B,CAC7B,gBAGG,oBAAqB,CACxB,WCNG,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,6BACA,AADA,kBACA,AADA,cACA,yBACA,AADA,sBACA,AADA,mBACA,WACA,eCJgB,CDKnB,cAGG,eACA,cAAe,CAClB,eAGG,cAAe,CAClB,KEdG,oBACA,AADA,oBACA,AADA,aACA,4BACA,AADA,6BACA,AADA,0BACA,AADA,sBACA,0BACA,AADA,uBACA,AADA,oBACA,gBACA,cACA,gBAAiB,CACpB,SAGG,cAAe,CAClB,WAIG,gBAAiB,CACpB,aCfG,iBAAkB,CACrB,qBAGG,iBAAkB,CACrB,2BAGG,WAAY,CACf,8BAGG,oBAAa,AAAb,oBAAa,AAAb,YAAa,CAChB,WAGG,kBACA,8BACA,AADA,6BACA,AADA,uBACA,AADA,mBACA,yBACA,AADA,sBACA,AADA,8BACA,sBACA,AADA,mBACA,AADA,qBACA,iBACA,YACA,WACA,UACA,WACA,uBACA,kBACA,2CACA,AADA,mCACA,YAAa,CAChB,0BAGG,WACA,WAAY,CACf,sBAGG,YAAa,CCnCjB,MACI,oBACA,AADA,oBACA,AADA,aACA,4BACA,AADA,6BACA,AADA,0BACA,AADA,sBACA,cAAe,CAClB,UAGG,eACA,eAAgB,CACnB,eAGG,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,6BACA,AADA,uBACA,AADA,mBACA,yBAA8B,AAA9B,sBAA8B,AAA9B,6BAA8B,CACjC,MAGG,WACA,UAAW,CACd,YCtBG,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,6BACA,AADA,uBACA,AADA,mBACA,8BACA,AADA,2BACA,AADA,6BACA,eACA,oBAAqB,CACxB,cCLG,eACA,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,8BACA,AADA,+BACA,AADA,2BACA,yBACA,AADA,sBACA,AADA,8BACA,eAAgB,CACnB,kBAGG,WACA,WAAY,CACf,UCVG,cAAe,CAClB,gBCDG,gBACA,cACA,gBAAiB,CACpB,OAGG,gBACA,cACA,oBACA,AADA,oBACA,AADA,aACA,4BACA,AADA,6BACA,AADA,0BACA,AADA,sBACA,yBAAmB,AAAnB,sBAAmB,AAAnB,kBAAmB,CACtB,qDCVG,YAAa,CAChB,sDCCG,iBAAkB,CACrB,gFAIG,kBACA,cACA,UACA,aACA,OACA,cACA,qBACA,yBACA,oBACA,4CACA,AADA,oCACA,yBACA,kCACA,WACA,cACA,0CAAkC,AAAlC,iCAAkC,CACrC,2BAGG,KACI,aACA,6BACA,wCACA,0BACA,8BAAkC,AAAlC,qBAAkC,CAGtC,GACI,aACA,kCACA,yBACA,WACA,4CAAgD,AAAhD,mCAAgD,CAAA,CAIxD,AApBC,mBAGG,KACI,aACA,6BACA,wCACA,0BACA,8BAAkC,AAAlC,qBAAkC,CAGtC,GACI,aACA,kCACA,yBACA,WACA,4CAAgD,AAAhD,mCAAgD,CAAA,CAIxD,aACI,kCACI,kCAAmC,CACtC,CC/CL,KACI,YAAa,CAChB,oBAGG,kBAAmB,CACtB,QAGG,y4HACA,wBACA,WACA,WAAY,CACf,UAGG,kBACA,MACA,OACA,iBACA,cAAe,CAClB,gBAGG,gBACA,gBAAiB,CACpB,KCzBG,gCACA,kBAAmB,CACtB,WAGG,8BACA,kBAAmB,CACtB,YAIG,iBAAkB,CACrB,aCZG,oBAAqB,CACxB,KAGG,oBAAqB,CACxB,MCHG,SACA,gBACA,SAAU,CACb,SAGG,WACA,oBAAqB,CACxB,kBAIG,wBACA,0BACA,mBACA,qBACA,cACA,mBACA,sBACA,kBACA,qBACA,qBACA,8BAAsB,AAAtB,qBAAsB,CACzB,YAGG,0BACA,uCACA,oCACA,oCACA,WACA,kBACA,QACA,KAAM,CACT,WAGG,4BACA,kBAAmB,CACtB,kBAGG,4BAA6B,CAChC","file":"app.css"}
{"version":3,"sources":["../../../resources/assets/sass/_border-box.scss","../../../resources/assets/sass/_base-font.scss","../../../resources/assets/sass/_header.scss","../../../resources/assets/sass/_variables.scss","../../../resources/assets/sass/_main.scss","../../../resources/assets/sass/_hovercard.scss","../../../resources/assets/sass/_notes.scss","../../../resources/assets/sass/_pagination.scss","../../../resources/assets/sass/_contacts-page.scss","../../../resources/assets/sass/_projects.scss","../../../resources/assets/sass/_footer.scss","../../../resources/assets/sass/_bridgy-links.scss","../../../resources/assets/sass/_emoji.scss","../../../resources/assets/sass/_mapbox.scss","../../../resources/assets/sass/_colors.scss","../../../resources/assets/sass/_styles.scss","../../../resources/assets/sass/_tags.scss"],"names":[],"mappings":"AAKA,KACI,8BAAsB,AAAtB,qBAAsB,CACzB,qBAKG,2BAAmB,AAAnB,kBAAmB,CACtB,KCVG,eACA,gCAAiC,CACpC,gBAGG,oBAAqB,CACxB,WCNG,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,6BACA,AADA,kBACA,AADA,cACA,yBACA,AADA,sBACA,AADA,mBACA,WACA,eCJgB,CDKnB,cAGG,eACA,cAAe,CAClB,eAGG,cAAe,CAClB,KEdG,oBACA,AADA,oBACA,AADA,aACA,4BACA,AADA,6BACA,AADA,0BACA,AADA,sBACA,0BACA,AADA,uBACA,AADA,oBACA,gBACA,cACA,gBAAiB,CACpB,SAGG,cAAe,CAClB,WAIG,gBAAiB,CACpB,aCfG,iBAAkB,CACrB,qBAGG,iBAAkB,CACrB,2BAGG,WAAY,CACf,WAGG,kBACA,8BACA,AADA,6BACA,AADA,uBACA,AADA,mBACA,yBACA,AADA,sBACA,AADA,8BACA,sBACA,AADA,mBACA,AADA,qBACA,iBACA,YACA,WACA,UACA,WACA,uBACA,kBACA,2CACA,AADA,mCACA,YAAa,CAChB,8BAGG,oBAAa,AAAb,oBAAa,AAAb,YAAa,CAChB,0BAGG,WACA,WAAY,CACf,sBAGG,YAAa,CCnCjB,MACI,oBACA,AADA,oBACA,AADA,aACA,4BACA,AADA,6BACA,AADA,0BACA,AADA,sBACA,cAAe,CAClB,UAGG,eACA,eAAgB,CACnB,eAGG,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,6BACA,AADA,uBACA,AADA,mBACA,yBAA8B,AAA9B,sBAA8B,AAA9B,6BAA8B,CACjC,MAGG,WACA,UAAW,CACd,YCtBG,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,6BACA,AADA,uBACA,AADA,mBACA,8BACA,AADA,2BACA,AADA,6BACA,eACA,oBAAqB,CACxB,cCLG,eACA,oBACA,AADA,oBACA,AADA,aACA,8BACA,AADA,8BACA,AADA,+BACA,AADA,2BACA,yBACA,AADA,sBACA,AADA,8BACA,eAAgB,CACnB,kBAGG,WACA,WAAY,CACf,UCVG,cAAe,CAClB,gBCDG,gBACA,cACA,gBAAiB,CACpB,OAGG,gBACA,cACA,oBACA,AADA,oBACA,AADA,aACA,4BACA,AADA,6BACA,AADA,0BACA,AADA,sBACA,yBAAmB,AAAnB,sBAAmB,AAAnB,kBAAmB,CACtB,qDCVG,YAAa,CAChB,2BCAG,iBAAkB,CACrB,gFAIG,kBACA,cACA,UACA,aACA,OACA,cACA,qBACA,yBACA,oBACA,4CACA,AADA,oCACA,yBACA,kCACA,WACA,cACA,0CAAkC,AAAlC,iCAAkC,CACrC,2BAGG,KACI,aACA,6BACA,wCACA,0BACA,8BAAkC,AAAlC,qBAAkC,CAGtC,GACI,aACA,kCACA,yBACA,WACA,4CAAgD,AAAhD,mCAAgD,CAAA,CAIxD,AApBC,mBAGG,KACI,aACA,6BACA,wCACA,0BACA,8BAAkC,AAAlC,qBAAkC,CAGtC,GACI,aACA,kCACA,yBACA,WACA,4CAAgD,AAAhD,mCAAgD,CAAA,CAIxD,aACI,kCACI,kCAAmC,CACtC,CC9CL,KACI,YAAa,CAChB,oBAGG,kBAAmB,CACtB,QAGG,y4HACA,wBACA,WACA,WAAY,CACf,UAGG,kBACA,MACA,OACA,iBACA,cAAe,CAClB,gBAGG,gBACA,gBAAiB,CACpB,KCzBG,gCACA,kBAAmB,CACtB,WAGG,8BACA,kBAAmB,CACtB,YAIG,iBAAkB,CACrB,KCZG,oBAAqB,CACxB,aAGG,oBAAqB,CACxB,MCHG,SACA,gBACA,SAAU,CACb,SAGG,WACA,oBAAqB,CACxB,kBAIG,wBACA,0BACA,mBACA,qBACA,cACA,mBACA,sBACA,kBACA,qBACA,qBACA,8BAAsB,AAAtB,qBAAsB,CACzB,YAGG,0BACA,uCACA,oCACA,oCACA,WACA,kBACA,QACA,KAAM,CACT,WAGG,4BACA,kBAAmB,CACtB,kBAGG,4BAA6B,CAChC","file":"app.css"}

View file

@ -223,6 +223,7 @@ a.mapboxgl-ctrl-logo {
border-color: #333;
padding: 0 5px;
color: #333;
box-sizing: border-box;
}
.mapboxgl-popup {

View file

@ -1 +1 @@
{"version":3,"sources":["webpack:///webpack/bootstrap b6efe62e7997f66fb20e","webpack:///colours.js"],"names":["__webpack_require__","moduleId","installedModules","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","configurable","enumerable","get","n","__esModule","object","property","prototype","hasOwnProperty","p","s","css","document","querySelector","getAttribute","split","pop","getElementById","value","form","childNodes","addEventListener","e","preventDefault","newCss","link","parts","push","setAttribute","join","formData","FormData","fetch","method","credentials","body","catch","error","console","warn"],"mappings":"mBAIA,SAAAA,oBAAAC,UAGA,GAAAC,iBAAAD,UACA,OAAAC,iBAAAD,UAAAE,QAGA,IAAAC,OAAAF,iBAAAD,WACAI,EAAAJ,SACAK,GAAA,EACAH,YAUA,OANAI,QAAAN,UAAAO,KAAAJ,OAAAD,QAAAC,OAAAA,OAAAD,QAAAH,qBAGAI,OAAAE,GAAA,EAGAF,OAAAD,QAvBA,IAAAD,oBA4BAF,oBAAAS,EAAAF,QAGAP,oBAAAU,EAAAR,iBAGAF,oBAAAW,EAAA,SAAAR,QAAAS,KAAAC,QACAb,oBAAAc,EAAAX,QAAAS,OACAG,OAAAC,eAAAb,QAAAS,MACAK,cAAA,EACAC,YAAA,EACAC,IAAAN,UAMAb,oBAAAoB,EAAA,SAAAhB,QACA,IAAAS,OAAAT,QAAAA,OAAAiB,WACA,WAA2B,OAAAjB,OAAA,SAC3B,WAAiC,OAAAA,QAEjC,OADAJ,oBAAAW,EAAAE,OAAA,IAAAA,QACAA,QAIAb,oBAAAc,EAAA,SAAAQ,OAAAC,UAAsD,OAAAR,OAAAS,UAAAC,eAAAjB,KAAAc,OAAAC,WAGtDvB,oBAAA0B,EAAA,GAGA1B,oBAAAA,oBAAA2B,EAAA,iEC3DA,IAEIC,IAFOC,SAASC,cAAc,iBAEnBC,aAAa,QAAQC,MAAM,KAAKC,MAG/CJ,SAASK,eAAe,sBAAsBC,MAAQP,IAGtD,IAAIQ,KAAOP,SAASK,eAAe,oBACzBE,KAAKC,WAAW,GACtBC,iBAAiB,QAAS,SAAUC,GACpCA,EAAEC,iBACF,IAAIC,OAASZ,SAASK,eAAe,sBAAsBC,MACvDO,KAAOb,SAASC,cAAc,iBAE9Ba,MADMD,KAAKX,aAAa,QACZC,MAAM,KACtBW,MAAMV,MACNU,MAAMC,KAAKH,QACXC,KAAKG,aAAa,OAAQF,MAAMG,KAAK,MACrC,IAAIC,SAAW,IAAIC,SAASZ,MAC5Ba,MAAM,yBACFC,OAAQ,OACRC,YAAa,cACbC,KAAML,WACPM,MAAM,SAAUC,OACfC,QAAQC,KAAKF","file":"public/assets/js/colours.js.map","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 4);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap b6efe62e7997f66fb20e","//colours.js\n\nlet link = document.querySelector('#colourScheme');\n\nlet css = link.getAttribute('href').split('/').pop();\n\n// update selected item in colour scheme list\ndocument.getElementById('colourSchemeSelect').value = css;\n\n// fix form\nlet form = document.getElementById('colourSchemeForm');\nlet btn = form.childNodes[5];\nbtn.addEventListener('click', function (e) {\n e.preventDefault();\n let newCss = document.getElementById('colourSchemeSelect').value;\n let link = document.querySelector('#colourScheme');\n let css = link.getAttribute('href');\n let parts = css.split('/');\n parts.pop();\n parts.push(newCss);\n link.setAttribute('href', parts.join('/'));\n let formData = new FormData(form);\n fetch('/update-colour-scheme', {\n method: 'POST',\n credentials: 'same-origin',\n body: formData\n }).catch(function (error) {\n console.warn(error);\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./colours.js"]}
{"version":3,"sources":["webpack:///webpack/bootstrap b56e9accee14dcede691","webpack:///colours.js"],"names":["__webpack_require__","moduleId","installedModules","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","configurable","enumerable","get","n","__esModule","object","property","prototype","hasOwnProperty","p","s","css","document","querySelector","getAttribute","split","pop","getElementById","value","form","childNodes","addEventListener","e","preventDefault","newCss","link","parts","push","setAttribute","join","formData","FormData","fetch","method","credentials","body","catch","error","console","warn"],"mappings":"mBAIA,SAAAA,oBAAAC,UAGA,GAAAC,iBAAAD,UACA,OAAAC,iBAAAD,UAAAE,QAGA,IAAAC,OAAAF,iBAAAD,WACAI,EAAAJ,SACAK,GAAA,EACAH,YAUA,OANAI,QAAAN,UAAAO,KAAAJ,OAAAD,QAAAC,OAAAA,OAAAD,QAAAH,qBAGAI,OAAAE,GAAA,EAGAF,OAAAD,QAvBA,IAAAD,oBA4BAF,oBAAAS,EAAAF,QAGAP,oBAAAU,EAAAR,iBAGAF,oBAAAW,EAAA,SAAAR,QAAAS,KAAAC,QACAb,oBAAAc,EAAAX,QAAAS,OACAG,OAAAC,eAAAb,QAAAS,MACAK,cAAA,EACAC,YAAA,EACAC,IAAAN,UAMAb,oBAAAoB,EAAA,SAAAhB,QACA,IAAAS,OAAAT,QAAAA,OAAAiB,WACA,WAA2B,OAAAjB,OAAA,SAC3B,WAAiC,OAAAA,QAEjC,OADAJ,oBAAAW,EAAAE,OAAA,IAAAA,QACAA,QAIAb,oBAAAc,EAAA,SAAAQ,OAAAC,UAAsD,OAAAR,OAAAS,UAAAC,eAAAjB,KAAAc,OAAAC,WAGtDvB,oBAAA0B,EAAA,GAGA1B,oBAAAA,oBAAA2B,EAAA,iEC3DA,IAEIC,IAFOC,SAASC,cAAc,iBAEnBC,aAAa,QAAQC,MAAM,KAAKC,MAG/CJ,SAASK,eAAe,sBAAsBC,MAAQP,IAGtD,IAAIQ,KAAOP,SAASK,eAAe,oBACzBE,KAAKC,WAAW,GACtBC,iBAAiB,QAAS,SAAUC,GACpCA,EAAEC,iBACF,IAAIC,OAASZ,SAASK,eAAe,sBAAsBC,MACvDO,KAAOb,SAASC,cAAc,iBAE9Ba,MADMD,KAAKX,aAAa,QACZC,MAAM,KACtBW,MAAMV,MACNU,MAAMC,KAAKH,QACXC,KAAKG,aAAa,OAAQF,MAAMG,KAAK,MACrC,IAAIC,SAAW,IAAIC,SAASZ,MAC5Ba,MAAM,yBACFC,OAAQ,OACRC,YAAa,cACbC,KAAML,WACPM,MAAM,SAAUC,OACfC,QAAQC,KAAKF","file":"public/assets/js/colours.js.map","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 4);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap b56e9accee14dcede691","//colours.js\n\nlet link = document.querySelector('#colourScheme');\n\nlet css = link.getAttribute('href').split('/').pop();\n\n// update selected item in colour scheme list\ndocument.getElementById('colourSchemeSelect').value = css;\n\n// fix form\nlet form = document.getElementById('colourSchemeForm');\nlet btn = form.childNodes[5];\nbtn.addEventListener('click', function (e) {\n e.preventDefault();\n let newCss = document.getElementById('colourSchemeSelect').value;\n let link = document.querySelector('#colourScheme');\n let css = link.getAttribute('href');\n let parts = css.split('/');\n parts.pop();\n parts.push(newCss);\n link.setAttribute('href', parts.join('/'));\n let formData = new FormData(form);\n fetch('/update-colour-scheme', {\n method: 'POST',\n credentials: 'same-origin',\n body: formData\n }).catch(function (error) {\n console.warn(error);\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./colours.js"]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more