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_ENV=testing
APP_DEBUG=true APP_DEBUG=true
APP_KEY=base64:6DJhvZLVjE6dD4Cqrteh+6Z5vZlG+v/soCKcDHLOAH0= APP_KEY=base64:6DJhvZLVjE6dD4Cqrteh+6Z5vZlG+v/soCKcDHLOAH0=
APP_URL=http://localhost:8000 APP_URL=http://jonnybarnes.localhost:8000
APP_LONGURL=localhost APP_LONGURL=jonnybarnes.localhost
APP_SHORTURL=local APP_SHORTURL=jmb.localhost
DB_CONNECTION=travis DB_CONNECTION=travis

View file

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

View file

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

View file

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

View file

@ -7,6 +7,9 @@ use Illuminate\Support\Facades\Route;
use Illuminate\Session\TokenMismatchException; use Illuminate\Session\TokenMismatchException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
/**
* @codeCoverageIgnore
*/
class Handler extends ExceptionHandler 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; use Exception;
class RemoteContentNotFound extends Exception class RemoteContentNotFoundException extends Exception
{ {
//used when guzzle cant find the remote content //used when guzzle cant find the remote content
} }

View file

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

View file

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

View file

@ -2,8 +2,8 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Contact;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use App\Models\Contact;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
@ -83,16 +83,14 @@ class ContactsController extends Controller
$contact->facebook = $request->input('facebook'); $contact->facebook = $request->input('facebook');
$contact->save(); $contact->save();
if ($request->hasFile('avatar')) { if ($request->hasFile('avatar') && ($request->input('homepage') != '')) {
if ($request->input('homepage') != '') { $dir = parse_url($request->input('homepage'), PHP_URL_HOST);
$dir = parse_url($request->input('homepage'))['host']; $destination = public_path() . '/assets/profile-images/' . $dir;
$destination = public_path() . '/assets/profile-images/' . $dir; $filesystem = new Filesystem();
$filesystem = new Filesystem(); if ($filesystem->isDirectory($destination) === false) {
if ($filesystem->isDirectory($destination) === false) { $filesystem->makeDirectory($destination);
$filesystem->makeDirectory($destination);
}
$request->file('avatar')->move($destination, 'image');
} }
$request->file('avatar')->move($destination, 'image');
} }
return redirect('/admin/contacts'); return redirect('/admin/contacts');
@ -123,37 +121,47 @@ class ContactsController extends Controller
*/ */
public function getAvatar($contactId) public function getAvatar($contactId)
{ {
// Initialising
$avatarURL = null;
$avatar = null;
$contact = Contact::findOrFail($contactId); $contact = Contact::findOrFail($contactId);
$homepage = $contact->homepage; if (mb_strlen($contact->homepage !== null) !== 0) {
if (($homepage !== null) && ($homepage !== '')) { $client = resolve(Client::class);
$client = new Client();
try { try {
$response = $client->get($homepage); $response = $client->get($contact->homepage);
$html = (string) $response->getBody();
$mf2 = \Mf2\parse($html, $homepage);
} catch (\GuzzleHttp\Exception\BadResponseException $e) { } 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) { foreach ($mf2['items'] as $microformat) {
if ($microformat['type'][0] == 'h-card') { if (array_get($microformat, 'type.0') == 'h-card') {
$avatarURL = $microformat['properties']['photo'][0]; $avatarURL = array_get($microformat, 'properties.photo.0');
break; break;
} }
} }
try { if ($avatarURL !== null) {
$avatar = $client->get($avatarURL); try {
} catch (\GuzzleHttp\Exception\BadResponseException $e) { $avatar = $client->get($avatarURL);
return "Unable to 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']; if ($avatar !== null) {
$filesystem = new Filesystem(); $directory = public_path() . '/assets/profile-images/' . parse_url($contact->homepage, PHP_URL_HOST);
if ($filesystem->isDirectory($directory) === false) { $filesystem = new Filesystem();
$filesystem->makeDirectory($directory); if ($filesystem->isDirectory($directory) === false) {
} $filesystem->makeDirectory($directory);
$filesystem->put($directory . '/image', $avatar->getBody()); }
$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; namespace App\Http\Controllers\Admin;
use App\Note; use App\Models\Note;
use Validator;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Jobs\SendWebMentions; use App\Jobs\SendWebMentions;
use App\Services\NoteService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
class NotesController extends Controller class NotesController extends Controller
{ {
protected $noteService;
public function __construct(NoteService $noteService)
{
$this->noteService = $noteService;
}
/** /**
* List the notes that can be edited. * List the notes that can be edited.
* *
@ -51,30 +42,10 @@ class NotesController extends Controller
*/ */
public function store(Request $request) public function store(Request $request)
{ {
$validator = Validator::make( Note::create([
$request->all(), 'in-reply-to' => $request->input('in-reply-to'),
['photo' => 'photosize'], 'note' => $request->input('content'),
['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);
return redirect('/admin/notes'); return redirect('/admin/notes');
} }

View file

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

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Article; use App\Models\Article;
use Jonnybarnes\IndieWeb\Numbers; use Jonnybarnes\IndieWeb\Numbers;
class ArticlesController extends Controller class ArticlesController extends Controller
@ -15,7 +15,7 @@ class ArticlesController extends Controller
public function index($year = null, $month = null) public function index($year = null, $month = null)
{ {
$articles = Article::where('published', '1') $articles = Article::where('published', '1')
->date($year, $month) ->date((int) $year, (int) $month)
->orderBy('updated_at', 'desc') ->orderBy('updated_at', 'desc')
->simplePaginate(5); ->simplePaginate(5);
@ -31,7 +31,7 @@ class ArticlesController extends Controller
{ {
$article = Article::where('titleurl', $slug)->firstOrFail(); $article = Article::where('titleurl', $slug)->firstOrFail();
if ($article->updated_at->year != $year || $article->updated_at->month != $month) { 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')); 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; namespace App\Http\Controllers;
use App\Bookmark; use App\Models\Bookmark;
class BookmarksController extends Controller class BookmarksController extends Controller
{ {

View file

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

View file

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

View file

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

View file

@ -2,320 +2,96 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Storage;
use Monolog\Logger; use Monolog\Logger;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use App\Jobs\ProcessImage; use App\Jobs\ProcessMedia;
use App\Services\LikeService; use App\Services\TokenService;
use App\Services\BookmarkService; use Illuminate\Http\UploadedFile;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use App\{Like, Media, Note, Place};
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\{Request, Response}; use Illuminate\Http\{Request, Response};
use App\Exceptions\InvalidTokenException; use App\Exceptions\InvalidTokenException;
use App\Models\{Like, Media, Note, Place};
use Phaza\LaravelPostgis\Geometries\Point; use Phaza\LaravelPostgis\Geometries\Point;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Intervention\Image\Exception\NotReadableException;
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException; use App\Services\Micropub\{HCardService, HEntryService, UpdateService};
use App\Services\{NoteService, PlaceService, TokenService};
class MicropubController extends Controller class MicropubController extends Controller
{ {
/**
* The Token service container.
*/
protected $tokenService; 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( public function __construct(
TokenService $tokenService, TokenService $tokenService,
NoteService $noteService, HEntryService $hentryService,
PlaceService $placeService HCardService $hcardService,
UpdateService $updateService
) { ) {
$this->tokenService = $tokenService; $this->tokenService = $tokenService;
$this->noteService = $noteService; $this->hentryService = $hentryService;
$this->placeService = $placeService; $this->hcardService = $hcardService;
$this->updateService = $updateService;
} }
/** /**
* This function receives an API request, verifies the authenticity * This function receives an API request, verifies the authenticity
* then passes over the info to the relavent Service class. * then passes over the info to the relavent Service class.
* *
* @param \Illuminate\Http\Request request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function post(Request $request) public function post()
{ {
try { try {
$tokenData = $this->tokenService->validateToken($request->bearerToken()); $tokenData = $this->tokenService->validateToken(request()->bearerToken());
} catch (InvalidTokenException $e) { } catch (InvalidTokenException $e) {
return response()->json([ return $this->invalidTokenResponse();
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
} }
// 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([ if ($tokenData->hasClaim('scope') === false) {
'response' => 'created', return $this->tokenHasNoScopeResponse();
'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);
return response()->json([ $this->logMicropubRequest(request()->all());
'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);
}
return response()->json([ if ((request()->input('h') == 'entry') || (request()->input('type.0') == 'h-entry')) {
'response' => 'created', if (stristr($tokenData->getClaim('scope'), 'create') === false) {
'location' => $note->longurl, return $this->insufficientScopeResponse();
], 201)->header('Location', $note->longurl);
} }
if ($request->input('h') == 'card' || $request->input('type')[0] == 'h-card') { $location = $this->hentryService->process(request()->all(), $this->getCLientId());
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->returnInsufficientScopeResponse();
}
$data = [];
if ($request->header('Content-Type') == 'application/json') {
$data['name'] = $request->input('properties.name');
$data['description'] = $request->input('properties.description') ?? null;
if ($request->has('properties.geo')) {
$data['geo'] = $request->input('properties.geo');
}
} else {
$data['name'] = $request->input('name');
$data['description'] = $request->input('description');
if ($request->has('geo')) {
$data['geo'] = $request->input('geo');
}
if ($request->has('latitude')) {
$data['latitude'] = $request->input('latitude');
$data['longitude'] = $request->input('longitude');
}
}
try {
$place = $this->placeService->createPlace($data);
} catch (\Exception $exception) {
return response()->json(['error' => true], 400);
}
return response()->json([ return response()->json([
'response' => 'created', 'response' => 'created',
'location' => $place->longurl, 'location' => $location,
], 201)->header('Location', $place->longurl); ], 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') { $location = $this->hcardService->process(request()->all());
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();
return response()->json([ return response()->json([
'response' => 'updated', 'response' => 'created',
]); 'location' => $location,
} ], 201)->header('Location', $location);
//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([ if (request()->input('action') == 'update') {
'response' => 'updated', if (stristr($tokenData->getClaim('scope'), 'update') === false) {
]); return $this->insufficientScopeResponse();
}
}
} }
return $this->updateService->process(request()->all());
} }
return response()->json([ return response()->json([
'response' => 'error', 'response' => 'error',
'error' => 'forbidden', 'error_description' => 'unsupported_request_type',
'error_description' => 'The token has no scopes', ], 500);
], 403);
} }
/** /**
@ -324,47 +100,37 @@ class MicropubController extends Controller
* appropriately. Further if the request has the query parameter * appropriately. Further if the request has the query parameter
* synidicate-to we respond with the known syndication endpoints. * synidicate-to we respond with the known syndication endpoints.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function get(Request $request) public function get()
{ {
try { try {
$tokenData = $this->tokenService->validateToken($request->bearerToken()); $tokenData = $this->tokenService->validateToken(request()->bearerToken());
} catch (InvalidTokenException $e) { } catch (InvalidTokenException $e) {
return response()->json([ return $this->invalidTokenResponse();
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
} }
//we have a valid token, is `syndicate-to` set?
if ($request->input('q') === 'syndicate-to') { if (request()->input('q') === 'syndicate-to') {
return response()->json([ return response()->json([
'syndicate-to' => config('syndication.targets'), 'syndicate-to' => config('syndication.targets'),
]); ]);
} }
//nope, how about a config query? if (request()->input('q') == 'config') {
if ($request->input('q') == 'config') {
return response()->json([ return response()->json([
'syndicate-to' => config('syndication.targets'), 'syndicate-to' => config('syndication.targets'),
'media-endpoint' => route('media-endpoint'), '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( preg_match_all(
'/([0-9\.\-]+)/', '/([0-9\.\-]+)/',
$request->input('q'), request()->input('q'),
$matches $matches
); );
$distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000; $distance = (count($matches[0]) == 3) ? 100 * $matches[0][2] : 1000;
$places = Place::near(new Point($matches[0][0], $matches[0][1]))->get(); $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([ return response()->json([
'response' => 'places', '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([ return response()->json([
'response' => 'token', 'response' => 'token',
'token' => [ 'token' => [
@ -386,77 +152,25 @@ class MicropubController extends Controller
/** /**
* Process a media item posted to the media endpoint. * Process a media item posted to the media endpoint.
* *
* @param Illuminate\Http\Request $request
* @return Illuminate\Http\Response * @return Illuminate\Http\Response
*/ */
public function media(Request $request) public function media()
{ {
try { try {
$tokenData = $this->tokenService->validateToken($request->bearerToken()); $tokenData = $this->tokenService->validateToken(request()->bearerToken());
} catch (InvalidTokenException $e) { } catch (InvalidTokenException $e) {
return response()->json([ return $this->invalidTokenResponse();
'response' => 'error',
'error' => 'invalid_token',
'error_description' => 'The provided token did not pass validation',
], 400);
} }
$logger = new Logger('micropub'); if ($tokenData->hasClaim('scope') === false) {
$logger->pushHandler(new StreamHandler(storage_path('logs/micropub.log')), Logger::DEBUG); return $this->tokenHasNoScopeResponse();
$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);
}
$size = $request->file('file')->getClientSize(); if (stristr($tokenData->getClaim('scope'), 'create') === false) {
Storage::disk('local')->put($filename, $request->file('file')->openFile()->fread($size)); return $this->insufficientScopeResponse();
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 ((request()->hasFile('file') && request()->file('file')->isValid()) === false) {
return response()->json([ return response()->json([
'response' => 'error', 'response' => 'error',
'error' => 'invalid_request', 'error' => 'invalid_request',
@ -464,11 +178,32 @@ class MicropubController extends Controller
], 400); ], 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([ return response()->json([
'response' => 'error', 'response' => 'created',
'error' => 'invalid_request', 'location' => $media->url,
'error_description' => 'The provided token has no scopes', ], 201)->header('Location', $media->url);
], 400);
} }
/** /**
@ -495,6 +230,7 @@ class MicropubController extends Controller
$videoMimeTypes = [ $videoMimeTypes = [
'video/mp4', 'video/mp4',
'video/mpeg', 'video/mpeg',
'video/ogg',
'video/quicktime', 'video/quicktime',
'video/webm', 'video/webm',
]; ];
@ -515,7 +251,29 @@ class MicropubController extends Controller
return 'download'; 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([ return response()->json([
'response' => 'error', 'response' => 'error',
@ -523,4 +281,22 @@ class MicropubController extends Controller
'error_description' => 'The tokens scope does not have the necessary requirements.', 'error_description' => 'The tokens scope does not have the necessary requirements.',
], 401); ], 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; namespace App\Http\Controllers;
use App\Note; use App\Models\Note;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Jonnybarnes\IndieWeb\Numbers; use Jonnybarnes\IndieWeb\Numbers;
use App\Services\ActivityStreamsService; 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; namespace App\Http\Controllers;
use App\Place; use App\Models\Place;
class PlacesController extends Controller class PlacesController extends Controller
{ {

View file

@ -2,7 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Note; use App\Models\Note;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class SearchController extends Controller class SearchController extends Controller
@ -10,16 +10,6 @@ class SearchController extends Controller
public function search(Request $request) public function search(Request $request)
{ {
$notes = Note::search($request->terms)->paginate(10); $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')); return view('search', compact('notes'));
} }

View file

@ -2,9 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\ShortURL;
use Jonnybanres\IndieWeb\Numbers;
class ShortURLsController extends Controller class ShortURLsController extends Controller
{ {
/* /*
@ -41,21 +38,11 @@ class ShortURLsController extends Controller
* *
* @return \Illuminate\Routing\RedirectResponse redirect * @return \Illuminate\Routing\RedirectResponse redirect
*/ */
public function googlePLus() public function googlePlus()
{ {
return redirect('https://plus.google.com/u/0/117317270900655269082/about'); 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. * Redirect a short url of this site out to a long one based on post type.
* Further redirects may happen. * Further redirects may happen.
@ -75,46 +62,4 @@ class ShortURLsController extends Controller
return redirect(config('app.url') . '/' . $type . '/' . $postId); 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; namespace App\Http\Controllers;
use App\Note; use App\Models\Note;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Jobs\ProcessWebMention; use App\Jobs\ProcessWebMention;
@ -36,35 +36,32 @@ class WebMentionsController extends Controller
$path = parse_url($request->input('target'), PHP_URL_PATH); $path = parse_url($request->input('target'), PHP_URL_PATH);
$pathParts = explode('/', $path); $pathParts = explode('/', $path);
switch ($pathParts[1]) { if ($pathParts[1] == 'notes') {
case 'notes': //we have a note
//we have a note $noteId = $pathParts[2];
$noteId = $pathParts[2]; $numbers = new Numbers();
$numbers = new Numbers(); try {
try { $note = Note::findOrFail($numbers->b60tonum($noteId));
$note = Note::findOrFail($numbers->b60tonum($noteId)); dispatch(new ProcessWebMention($note, $request->input('source')));
dispatch(new ProcessWebMention($note, $request->input('source'))); } catch (ModelNotFoundException $e) {
} catch (ModelNotFoundException $e) { return new Response('This note doesnt exist.', 400);
return new Response('This note doesnt exist.', 400); }
}
return new Response( return new Response(
'Webmention received, it will be processed shortly', 'Webmention received, it will be processed shortly',
202 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;
} }
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, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\LinkHeadersMiddleware::class, \App\Http\Middleware\LinkHeadersMiddleware::class,
//\App\Http\Middleware\DevTokenMiddleware::class,
\App\Http\Middleware\LocalhostSessionMiddleware::class, \App\Http\Middleware\LocalhostSessionMiddleware::class,
\App\Http\Middleware\ActivityStreamLinks::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 Closure;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
/**
* @codeCoverageIgnore
*/
class RedirectIfAuthenticated class RedirectIfAuthenticated
{ {
/** /**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@ namespace App\Jobs;
use Mf2; use Mf2;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use App\{Note, WebMention};
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Models\{Note, WebMention};
use Jonnybarnes\WebmentionsParser\Parser; use Jonnybarnes\WebmentionsParser\Parser;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -41,23 +41,25 @@ class ProcessWebMention implements ShouldQueue
*/ */
public function handle(Parser $parser, Client $guzzle) public function handle(Parser $parser, Client $guzzle)
{ {
$remoteContent = $this->getRemoteContent($this->source, $guzzle); try {
if ($remoteContent === null) { $response = $guzzle->request('GET', $this->source);
} catch (RequestException $e) {
throw new RemoteContentNotFoundException; 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(); $webmentions = WebMention::where('source', $this->source)->get();
foreach ($webmentions as $webmention) { foreach ($webmentions as $webmention) {
//check webmention still references target // check webmention still references target
//we try each type of mention (reply/like/repost) // we try each type of mention (reply/like/repost)
if ($webmention->type == 'in-reply-to') { if ($webmention->type == 'in-reply-to') {
if ($parser->checkInReplyTo($microformats, $this->note->longurl) == false) { if ($parser->checkInReplyTo($microformats, $this->note->longurl) == false) {
//it doesn't so delete // it doesnt so delete
$webmention->delete(); $webmention->delete();
return; return;
} }
//webmenion is still a reply, so update content // webmenion is still a reply, so update content
dispatch(new SaveProfileImage($microformats)); dispatch(new SaveProfileImage($microformats));
$webmention->mf2 = json_encode($microformats); $webmention->mf2 = json_encode($microformats);
$webmention->save(); $webmention->save();
@ -66,25 +68,25 @@ class ProcessWebMention implements ShouldQueue
} }
if ($webmention->type == 'like-of') { if ($webmention->type == 'like-of') {
if ($parser->checkLikeOf($microformats, $note->longurl) == false) { if ($parser->checkLikeOf($microformats, $note->longurl) == false) {
//it doesn't so delete // it doesnt so delete
$webmention->delete(); $webmention->delete();
return; 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 ($webmention->type == 'repost-of') {
if ($parser->checkRepostOf($microformats, $note->longurl) == false) { if ($parser->checkRepostOf($microformats, $note->longurl) == false) {
//it doesn't so delete // it doesnt so delete
$webmention->delete(); $webmention->delete();
return; 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(); $webmention = new WebMention();
$type = $parser->getMentionType($microformats); //throw error here? $type = $parser->getMentionType($microformats); // throw error here?
dispatch(new SaveProfileImage($microformats)); dispatch(new SaveProfileImage($microformats));
$webmention->source = $this->source; $webmention->source = $this->source;
$webmention->target = $this->note->longurl; $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 string $url
* @param GuzzleHttp\client $guzzle
* @return string|null * @return string|null
*/ */
private function getRemoteContent($url, Client $guzzle) private function saveRemoteContent($html, $url)
{ {
try { $filenameFromURL = str_replace(
$response = $guzzle->request('GET', $url); ['https://', 'http://'],
} catch (RequestException $e) { ['https/', 'http/'],
return; $url
);
if (substr($url, -1) == '/') {
$filenameFromURL .= 'index.html';
} }
$html = (string) $response->getBody(); $path = storage_path() . '/HTML/' . $filenameFromURL;
$path = storage_path() . '/HTML/' . $this->createFilenameFromURL($url);
$parts = explode('/', $path); $parts = explode('/', $path);
$name = array_pop($parts); $name = array_pop($parts);
$dir = implode('/', $parts); $dir = implode('/', $parts);
@ -118,24 +122,5 @@ class ProcessWebMention implements ShouldQueue
mkdir($dir, 0755, true); mkdir($dir, 0755, true);
} }
file_put_contents("$dir/$name", $html); 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 //dont save pbs.twimg.com links
if (parse_url($photo, PHP_URL_HOST) != 'pbs.twimg.com' if (parse_url($photo, PHP_URL_HOST) != 'pbs.twimg.com'
&& parse_url($photo, PHP_URL_HOST) != 'twitter.com') { && parse_url($photo, PHP_URL_HOST) != 'twitter.com') {
$client = new Client(); $client = resolve(Client::class);
try { try {
$response = $client->get($photo); $response = $client->get($photo);
$image = $response->getBody(true); $image = $response->getBody(true);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
<?php <?php
namespace App; namespace App\Models;
use DB; use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
@ -53,7 +53,7 @@ class Place extends Model
*/ */
public function notes() 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) 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([ 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; 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) public function setExternalUrlsAttribute($url)
{ {
$type = $this->getType($url); if ($url === null) {
if ($type === null) { return;
throw new \Exception('Unkown external url type ' . $url);
} }
$type = $this->getType($url);
$already = []; $already = [];
if (array_key_exists('external_urls', $this->attributes)) { if (array_key_exists('external_urls', $this->attributes)) {
$already = json_decode($this->attributes['external_urls'], true); $already = json_decode($this->attributes['external_urls'], true);
@ -155,6 +158,6 @@ class Place extends Model
return 'osm'; return 'osm';
} }
return null; return 'default';
} }
} }

View file

@ -1,11 +1,18 @@
<?php <?php
namespace App; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Tag extends 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. * Define the relationship with tags.
* *
@ -13,7 +20,7 @@ class Tag extends Model
*/ */
public function notes() 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() 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. * Normalize tags so theyre lowercase and fancy diatrics are removed.
* *

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,29 +4,25 @@ declare(strict_types=1);
namespace App\Services; namespace App\Services;
use App\Like; use App\Models\Like;
use App\Jobs\ProcessLike; use App\Jobs\ProcessLike;
use Illuminate\Http\Request;
class LikeService class LikeService
{ {
/** /**
* Create a new Like. * 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 //micropub request
$url = normalize_url($request->input('properties.like-of.0')); $url = normalize_url(array_get($request, 'properties.like-of.0'));
} }
if ( if (array_get($request, 'like-of')) {
($request->header('Content-Type') == 'x-www-url-formencoded') $url = normalize_url(array_get($request, 'like-of'));
||
($request->header('Content-Type') == 'multipart/form-data')
) {
$url = normalize_url($request->input('like-of'));
} }
$like = Like::create(['url' => $url]); $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; namespace App\Services;
use App\{Media, Note, Place}; use App\Models\{Media, Note, Place};
use App\Jobs\{SendWebMentions, SyndicateNoteToFacebook, SyndicateNoteToTwitter}; use App\Jobs\{SendWebMentions, SyndicateNoteToFacebook, SyndicateNoteToTwitter};
class NoteService class NoteService
@ -12,91 +12,38 @@ class NoteService
/** /**
* Create a new note. * Create a new note.
* *
* @param array $data * @param array $request
* @param string $client
* @return \App\Note $note * @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 = Note::create(
[ [
'note' => $data['content'], 'note' => $this->getContent($request),
'in_reply_to' => $data['in-reply-to'], 'in_reply_to' => $this->getInReplyTo($request),
'client_id' => $data['client-id'], 'client_id' => $client,
] ]
); );
if (array_key_exists('published', $data) && empty($data['published']) === false) { if ($this->getPublished($request)) {
$carbon = carbon($data['published']); $note->created_at = $note->updated_at = $this->getPublished($request);
$note->created_at = $note->updated_at = $carbon->toDateTimeString();
} }
if (array_key_exists('location', $data) && $data['location'] !== null && $data['location'] !== 'no-location') { $note->location = $this->getLocation($request);
if (starts_with($data['location'], config('app.url'))) {
//uri of form http://host/places/slug, we want slug if ($this->getCheckin($request)) {
//get the URL path, then take last part, we can hack with basename $note->place()->associate($this->getCheckin($request));
//as path looks like file path. $note->swarm_url = $this->getSwarmUrl($request);
$place = Place::where('slug', basename(parse_url($data['location'], PHP_URL_PATH)))->first(); if ($note->note === null || $note->note == '') {
$note->place()->associate($place); $note->note = 'Ive just checked in with Swarm';
}
if (substr($data['location'], 0, 4) == 'geo:') {
preg_match_all(
'/([0-9\.\-]+)/',
$data['location'],
$matches
);
$note->location = $matches[0][0] . ', ' . $matches[0][1];
} }
} }
if (array_key_exists('checkin', $data) && $data['checkin'] !== null) { $note->instagram_url = $this->getInstagramUrl($request);
$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';
}
}
}
/* drop image support for now foreach ($this->getMedia($request) as $media) {
//add images to media library $note->media()->save($media);
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'];
}
} }
$note->save(); $note->save();
@ -104,13 +51,175 @@ class NoteService
dispatch(new SendWebMentions($note)); dispatch(new SendWebMentions($note));
//syndication targets //syndication targets
if (in_array('twitter', $data['syndicate'])) { if (count($this->getSyndicationTargets($request)) > 0) {
dispatch(new SyndicateNoteToTwitter($note)); if (in_array('twitter', $this->getSyndicationTargets($request))) {
} dispatch(new SyndicateNoteToTwitter($note));
if (in_array('facebook', $data['syndicate'])) { }
dispatch(new SyndicateNoteToFacebook($note)); if (in_array('facebook', $this->getSyndicationTargets($request))) {
dispatch(new SyndicateNoteToFacebook($note));
}
} }
return $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; namespace App\Services;
use App\Place; use App\Models\Place;
use Phaza\LaravelPostgis\Geometries\Point; use Phaza\LaravelPostgis\Geometries\Point;
class PlaceService class PlaceService
@ -19,7 +19,7 @@ class PlaceService
{ {
//obviously a place needs a lat/lng, but this could be sent in a geo-url //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 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( preg_match_all(
'/([0-9\.\-]+)/', '/([0-9\.\-]+)/',
$data['geo'], $data['geo'],
@ -46,24 +46,24 @@ class PlaceService
public function createPlaceFromCheckin(array $checkin): Place public function createPlaceFromCheckin(array $checkin): Place
{ {
//check if the place exists if from swarm //check if the place exists if from swarm
if (array_key_exists('url', $checkin['properties'])) { if (array_has($checkin, 'properties.url')) {
$place = Place::whereExternalURL($checkin['properties']['url'][0])->get(); $place = Place::whereExternalURL(array_get($checkin, 'properties.url.0'))->get();
if (count($place) === 1) { if (count($place) === 1) {
return $place->first(); return $place->first();
} }
} }
if (array_key_exists('name', $checkin['properties']) === false) { if (array_has($checkin, 'properties.name') === false) {
throw new \InvalidArgumentException('Missing required name'); 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'); throw new \InvalidArgumentException('Missing required longitude/latitude');
} }
$place = new Place(); $place = new Place();
$place->name = $checkin['properties']['name'][0]; $place->name = array_get($checkin, 'properties.name.0');
$place->external_urls = $checkin['properties']['url'][0]; $place->external_urls = array_get($checkin, 'properties.url.0');
$place->location = new Point( $place->location = new Point(
(float) $checkin['properties']['latitude'][0], (float) array_get($checkin, 'properties.latitude.0'),
(float) $checkin['properties']['longitude'][0] (float) array_get($checkin, 'properties.longitude.0')
); );
$place->save(); $place->save();

View file

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

View file

@ -1,5 +1,10 @@
# Changelog # 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) ## Version 0.13.1 (2017-11-20)
- A small fix when adding a new bookmark - A small fix when adding a new bookmark

View file

@ -35,12 +35,14 @@
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "~3.0", "barryvdh/laravel-debugbar": "~3.0",
"bmitch/churn-php": "^0.2.0", "bmitch/churn-php": "^0.2.0",
"codedungeon/phpunit-result-printer": "^0.3.0",
"filp/whoops": "~2.0", "filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4", "fzaninotto/faker": "~1.4",
"jakub-onderka/php-parallel-lint": "^0.9.2", "jakub-onderka/php-parallel-lint": "^0.9.2",
"laravel/dusk": "^2.0", "laravel/dusk": "^2.0",
"mockery/mockery": "0.9.*", "mockery/mockery": "0.9.*",
"nunomaduro/collision": "^1.1", "nunomaduro/collision": "^1.1",
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "~6.0", "phpunit/phpunit": "~6.0",
"sebastian/phpcpd": "^3.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 <?php
use App\Models\Bookmark;
use Faker\Generator as Faker; use Faker\Generator as Faker;
$factory->define(App\Bookmark::class, function (Faker $faker) { $factory->define(Bookmark::class, function (Faker $faker) {
return [ return [
'url' => $faker->url, 'url' => $faker->url,
'name' => $faker->sentence, 'name' => $faker->sentence,

View file

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

View file

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

View file

@ -1,8 +1,9 @@
<?php <?php
use App\Models\Tag;
use Faker\Generator as Faker; use Faker\Generator as Faker;
$factory->define(App\Tag::class, function (Faker $faker) { $factory->define(Tag::class, function (Faker $faker) {
return [ return [
'tag' => $faker->word, '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 <?php
use App\Article; use App\Models\Article;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class ArticlesTableSeeder extends Seeder class ArticlesTableSeeder extends Seeder

View file

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

View file

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

View file

@ -1,5 +1,7 @@
<?php <?php
use App\Models\Like;
use Faker\Generator;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class LikesTableSeeder extends Seeder class LikesTableSeeder extends Seeder
@ -11,6 +13,16 @@ class LikesTableSeeder extends Seeder
*/ */
public function run() 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 <?php
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use App\Models\{Media, Note, Place};
class NotesTableSeeder extends Seeder class NotesTableSeeder extends Seeder
{ {
@ -11,26 +12,31 @@ class NotesTableSeeder extends Seeder
*/ */
public function run() public function run()
{ {
factory(App\Note::class, 10)->create(); factory(Note::class, 10)->create();
sleep(1); 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. 🍺', 'note' => 'Having a #beer at the local. 🍺',
]); ]);
$noteWithPlace->tweet_id = '123456789'; $noteWithPlace->tweet_id = '123456789';
$place = App\Place::find(1); $place = Place::find(1);
$noteWithPlace->place()->associate($place); $noteWithPlace->place()->associate($place);
$noteWithPlace->save(); $noteWithPlace->save();
sleep(1); sleep(1);
$noteWithContact = App\Note::create([ $noteWithContact = Note::create([
'note' => 'Hi @tantek' 'note' => 'Hi @tantek'
]); ]);
sleep(1); sleep(1);
$noteWithContactPlusPic = App\Note::create([ $noteWithContactPlusPic = Note::create([
'note' => 'Hi @aaron', 'note' => 'Hi @aaron',
'client_id' => 'https://jbl5.dev/notes/new' 'client_id' => 'https://jbl5.dev/notes/new'
]); ]);
sleep(1); sleep(1);
$noteWithoutContact = App\Note::create([ $noteWithoutContact = Note::create([
'note' => 'Hi @bob', 'note' => 'Hi @bob',
'client_id' => 'https://quill.p3k.io' 'client_id' => 'https://quill.p3k.io'
]); ]);
@ -41,13 +47,31 @@ class NotesTableSeeder extends Seeder
mkdir(public_path() . '/assets/profile-images/aaronparecki.com', 0755); mkdir(public_path() . '/assets/profile-images/aaronparecki.com', 0755);
copy(base_path() . '/tests/aaron.png', public_path() . '/assets/profile-images/aaronparecki.com/image'); copy(base_path() . '/tests/aaron.png', public_path() . '/assets/profile-images/aaronparecki.com/image');
} }
$noteWithCoords = App\Note::create([ $noteWithCoords = Note::create([
'note' => 'Note from somehwere', 'note' => 'Note from a town',
]); ]);
$noteWithCoords->location = '53.499,-2.379'; $noteWithCoords->location = '53.499,-2.379';
$noteWithCoords->save(); $noteWithCoords->save();
sleep(1); 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', 'note' => 'This note has all the syndication targets',
]); ]);
$noteSyndicated->tweet_id = '123456'; $noteSyndicated->tweet_id = '123456';
@ -56,8 +80,29 @@ class NotesTableSeeder extends Seeder
$noteSyndicated->instagram_url = 'https://www.instagram.com/p/aWsEd123Jh'; $noteSyndicated->instagram_url = 'https://www.instagram.com/p/aWsEd123Jh';
$noteSyndicated->save(); $noteSyndicated->save();
sleep(1); 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 '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 <?php
use App\Place; use App\Models\Place;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Phaza\LaravelPostgis\Geometries\Point; use Phaza\LaravelPostgis\Geometries\Point;

View file

@ -1,6 +1,6 @@
<?php <?php
use App\WebMention; use App\Models\WebMention;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class WebMentionsTableSeeder extends Seeder class WebMentionsTableSeeder extends Seeder
@ -12,13 +12,21 @@ class WebMentionsTableSeeder extends Seeder
*/ */
public function run() public function run()
{ {
$webmention = WebMention::create([ $webmentionAaron = WebMention::create([
'source' => 'https://aaornpk.localhost/reply/1', 'source' => 'https://aaronpk.localhost/reply/1',
'target' => 'https://jonnybarnes.localhost/notes/D', 'target' => config('app.url') . '/notes/E',
'commentable_id' => '13', 'commentable_id' => '14',
'commentable_type' => 'App\Note', 'commentable_type' => 'App\Models\Note',
'type' => 'in-reply-to', '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", "license": "CC0-1.0",
"dependencies": { "dependencies": {
"alertify.js": "^1.0.12", "alertify.js": "^1.0.12",
"mapbox-gl": "^0.42.0", "mapbox-gl": "^0.42.2",
"marked": "^0.3.6", "marked": "^0.3.7",
"normalize.css": "^7.0.0" "normalize.css": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"ajv": "^5.3.0", "ajv": "^5.5.2",
"autoprefixer": "^7.1.6", "autoprefixer": "^7.2.3",
"babel-cli": "^6.26.0", "babel-cli": "^6.26.0",
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
"babel-loader": "^7.1.2", "babel-loader": "^7.1.2",
@ -21,22 +21,23 @@
"babel-preset-latest": "^6.16.0", "babel-preset-latest": "^6.16.0",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"dotenv-webpack": "^1.5.4", "dotenv-webpack": "^1.5.4",
"eslint": "^4.11.0", "eslint": "^4.13.1",
"eslint-config-standard": "^10.2.1", "eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.8.0", "eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1", "eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0", "eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
"husky": "^0.14.3", "husky": "^0.15.0-beta.16",
"lint-staged": "^5.0.0", "lint-staged": "^6.0.0",
"postcss-cli": "^4.1.1", "postcss-cli": "^4.1.1",
"postcss-sass": "^0.2.0",
"pre-commit": "^1.1.3", "pre-commit": "^1.1.3",
"source-list-map": "^2.0.0", "source-list-map": "^2.0.0",
"stylelint": "^8.2.0", "stylelint": "^8.4.0",
"stylelint-config-standard": "^17.0.0", "stylelint-config-standard": "^18.0.0",
"uglify-js": "^3.1.9", "uglify-js": "^3.2.2",
"webpack": "^3.8.1", "webpack": "^3.10.0",
"webpack-sources": "^1.0.2" "webpack-sources": "^1.1.0"
}, },
"scripts": { "scripts": {
"compress": "scripts/compress", "compress": "scripts/compress",
@ -47,17 +48,21 @@
"make:css": "npm run lint:sass && npm run sass && npm run postcss", "make:css": "npm run lint:sass && npm run sass && npm run postcss",
"make:js": "npm run lint:es6 && npm run webpack && npm run uglifyjs", "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", "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", "sass": "sassc --style compressed --sourcemap resources/assets/sass/app.scss public/assets/css/app.css",
"uglifyjs": "scripts/uglifyjs", "uglifyjs": "scripts/uglifyjs",
"webpack": "webpack --progress --colors" "webpack": "webpack --progress --colors"
}, },
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": { "lint-staged": {
"resources/assets/es6/*.js": [ "./resources/assets/es6/*.js": [
"eslint --fix", "eslint --fix",
"git add" "git add"
], ],
"resources/assets/sass/**/*.scss": [ "*.scss": [
"stylelint --syntax=scss --fix", "stylelint --syntax=scss --fix",
"git add" "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" convertNoticesToExceptions="true"
convertWarningsToExceptions="true" convertWarningsToExceptions="true"
processIsolation="false" processIsolation="false"
stopOnFailure="false"> stopOnFailure="false"
printerClass="Codedungeon\PHPUnitPrettyResultPrinter\Printer">
<testsuites> <testsuites>
<testsuite name="Feature"> <testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory> <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; border-color: #333;
padding: 0 5px; padding: 0 5px;
color: #333; color: #333;
box-sizing: border-box;
} }
.mapboxgl-popup { .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