diff --git a/app/Console/Commands/Installer.php b/app/Console/Commands/Installer.php new file mode 100644 index 000000000..50bd01205 --- /dev/null +++ b/app/Console/Commands/Installer.php @@ -0,0 +1,165 @@ +welcome(); + } + + protected function welcome() + { + $this->info(' ____ _ ______ __ '); + $this->info(' / __ \(_) _____ / / __/__ ____/ / '); + $this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / '); + $this->info(' / ____/ /> info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ '); + $this->info(' '); + $this->info(' Welcome to the Pixelfed Installer!'); + $this->info(' '); + $this->info(' '); + $this->info('Pixelfed version: ' . config('pixelfed.version')); + $this->line(' '); + $this->info('Scanning system...'); + $this->preflightCheck(); + } + protected function preflightCheck() + { + $this->line(' '); + $this->info('Checking for installed dependencies...'); + $redis = Redis::connection(); + if($redis->ping()) { + $this->info('- Found redis!'); + } else { + $this->error('- Redis not found, aborting installation'); + exit; + } + $this->checkPhpDependencies(); + $this->checkPermissions(); + $this->envCheck(); + } + + protected function checkPhpDependencies() + { + $extensions = [ + 'bcmath', + 'ctype', + 'curl', + 'json', + 'mbstring', + 'openssl' + ]; + $this->line(''); + $this->info('Checking for required php extensions...'); + foreach($extensions as $ext) { + if(extension_loaded($ext) == false) { + $this->error("- {$ext} extension not found, aborting installation"); + exit; + } else { + $this->info("- {$ext} extension found!"); + } + } + } + + protected function checkPermissions() + { + $this->line(''); + $this->info('Checking for proper filesystem permissions...'); + + $paths = [ + base_path('bootstrap'), + base_path('storage') + ]; + + foreach($paths as $path) { + if(is_writeable($path) == false) { + $this->error("- Invalid permission found! Aborting installation."); + $this->error(" Please make the following path writeable by the web server:"); + $this->error(" $path"); + exit; + } else { + $this->info("- Found valid permissions for {$path}"); + } + } + } + + protected function envCheck() + { + if(!file_exists(base_path('.env'))) { + $this->line(''); + $this->info('No .env configuration file found. We will create one now!'); + $this->createEnv(); + } else { + $confirm = $this->confirm('Found .env file, do you want to overwrite it?'); + if(!$confirm) { + $this->info('Cancelling installation.'); + exit; + } + $confirm = $this->confirm('Are you really sure you want to overwrite it?'); + if(!$confirm) { + $this->info('Cancelling installation.'); + exit; + } + $this->error('Warning ... if you did not backup your .env before its overwritten it will be permanently deleted.'); + $confirm = $this->confirm('The application may be installed already, are you really sure you want to overwrite it?'); + if(!$confirm) { + $this->info('Cancelling installation.'); + exit; + } + } + $this->postInstall(); + } + + protected function createEnv() + { + $this->line(''); + // copy env + $name = $this->ask('Site name [ex: Pixelfed]'); + $domain = $this->ask('Site Domain [ex: pixelfed.com]'); + $tls = $this->choice('Use HTTPS/TLS?', ['https', 'http'], 0); + $dbDrive = $this->choice('Select database driver', ['mysql', 'pgsql'/*, 'sqlite', 'sqlsrv'*/], 0); + $ws = $this->choice('Select cache driver', ["apc", "array", "database", "file", "memcached", "redis"], 5); + + } + + protected function postInstall() + { + $this->callSilent('config:cache'); + //$this->call('route:cache'); + $this->info('Pixelfed has been successfully installed!'); + } +} diff --git a/app/Events/NewMention.php b/app/Events/NewMention.php new file mode 100644 index 000000000..90adcc0d8 --- /dev/null +++ b/app/Events/NewMention.php @@ -0,0 +1,51 @@ +user = $user; + $this->data = $data; + } + + public function broadcastAs() + { + return 'notification.new.mention'; + } + + public function broadcastOn() + { + return new PrivateChannel('App.User.' . $this->user->id); + } + + public function broadcastWith() + { + return ['id' => $this->user->id]; + } + + public function via() + { + return 'broadcast'; + } +} diff --git a/app/Events/Notification/NewPublicPost.php b/app/Events/Notification/NewPublicPost.php new file mode 100644 index 000000000..e43b1f19b --- /dev/null +++ b/app/Events/Notification/NewPublicPost.php @@ -0,0 +1,57 @@ +status = $status; + } + + public function broadcastAs() + { + return 'status'; + } + + public function broadcastOn() + { + return new Channel('firehost.public'); + } + + public function broadcastWith() + { + $resource = new Fractal\Resource\Item($this->status, new StatusTransformer()); + $res = $this->fractal->createData($resource)->toArray(); + return [ + 'entity' => $res + ]; + } + + public function via() + { + return 'broadcast'; + } +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 980dd4dcd..713e037cc 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -128,7 +128,7 @@ class AccountController extends Controller } } - public function fetchNotifications($id) + public function fetchNotifications(int $id) { $key = config('cache.prefix').":user.{$id}.notifications"; $redis = Redis::connection(); @@ -167,14 +167,14 @@ class AccountController extends Controller public function mute(Request $request) { $this->validate($request, [ - 'type' => 'required|string', + 'type' => 'required|alpha_dash', 'item' => 'required|integer|min:1', ]); $user = Auth::user()->profile; $type = $request->input('type'); $item = $request->input('item'); - $action = "{$type}.mute"; + $action = $type . '.mute'; if (!in_array($action, $this->filters)) { return abort(406); @@ -211,17 +211,71 @@ class AccountController extends Controller return redirect()->back(); } - public function block(Request $request) + public function unmute(Request $request) { $this->validate($request, [ - 'type' => 'required|string', + 'type' => 'required|alpha_dash', 'item' => 'required|integer|min:1', ]); $user = Auth::user()->profile; $type = $request->input('type'); $item = $request->input('item'); - $action = "{$type}.block"; + $action = $type . '.mute'; + + if (!in_array($action, $this->filters)) { + return abort(406); + } + $filterable = []; + switch ($type) { + case 'user': + $profile = Profile::findOrFail($item); + if ($profile->id == $user->id) { + return abort(403); + } + $class = get_class($profile); + $filterable['id'] = $profile->id; + $filterable['type'] = $class; + break; + + default: + abort(400); + break; + } + + $filter = UserFilter::whereUserId($user->id) + ->whereFilterableId($filterable['id']) + ->whereFilterableType($filterable['type']) + ->whereFilterType('mute') + ->first(); + + if($filter) { + $filter->delete(); + } + + $pid = $user->id; + Cache::forget("user:filter:list:$pid"); + Cache::forget("feature:discover:people:$pid"); + Cache::forget("feature:discover:posts:$pid"); + + if($request->wantsJson()) { + return response()->json([200]); + } else { + return redirect()->back(); + } + } + + public function block(Request $request) + { + $this->validate($request, [ + 'type' => 'required|alpha_dash', + 'item' => 'required|integer|min:1', + ]); + + $user = Auth::user()->profile; + $type = $request->input('type'); + $item = $request->input('item'); + $action = $type.'.block'; if (!in_array($action, $this->filters)) { return abort(406); } @@ -259,6 +313,56 @@ class AccountController extends Controller return redirect()->back(); } + + public function unblock(Request $request) + { + $this->validate($request, [ + 'type' => 'required|alpha_dash', + 'item' => 'required|integer|min:1', + ]); + + $user = Auth::user()->profile; + $type = $request->input('type'); + $item = $request->input('item'); + $action = $type . '.block'; + if (!in_array($action, $this->filters)) { + return abort(406); + } + $filterable = []; + switch ($type) { + case 'user': + $profile = Profile::findOrFail($item); + if ($profile->id == $user->id) { + return abort(403); + } + $class = get_class($profile); + $filterable['id'] = $profile->id; + $filterable['type'] = $class; + break; + + default: + abort(400); + break; + } + + + $filter = UserFilter::whereUserId($user->id) + ->whereFilterableId($filterable['id']) + ->whereFilterableType($filterable['type']) + ->whereFilterType('block') + ->first(); + + if($filter) { + $filter->delete(); + } + + $pid = $user->id; + Cache::forget("user:filter:list:$pid"); + Cache::forget("feature:discover:people:$pid"); + Cache::forget("feature:discover:posts:$pid"); + return redirect()->back(); + } + public function followRequests(Request $request) { $pid = Auth::user()->profile->id; diff --git a/app/Http/Controllers/ApiController.php b/app/Http/Controllers/ApiController.php index 51f42dabd..c18c0505b 100644 --- a/app/Http/Controllers/ApiController.php +++ b/app/Http/Controllers/ApiController.php @@ -31,6 +31,10 @@ class ApiController extends BaseApiController 'media_types' => config('pixelfed.media_types'), 'enforce_account_limit' => config('pixelfed.enforce_account_limit') + ], + 'activitypub' => [ + 'enabled' => config('pixelfed.activitypub_enabled'), + 'remote_follow' => config('pixelfed.remote_follow_enabled') ] ]; }); diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 703241ce7..0a8e3b662 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Auth; +use DB; use Cache; use App\Comment; @@ -58,14 +59,21 @@ class CommentController extends Controller Cache::forget('transform:status:'.$status->url()); - $autolink = Autolink::create()->autolink($comment); - $reply = new Status(); - $reply->profile_id = $profile->id; - $reply->caption = e($comment); - $reply->rendered = $autolink; - $reply->in_reply_to_id = $status->id; - $reply->in_reply_to_profile_id = $status->profile_id; - $reply->save(); + $reply = DB::transaction(function() use($comment, $status, $profile) { + $autolink = Autolink::create()->autolink($comment); + $reply = new Status(); + $reply->profile_id = $profile->id; + $reply->caption = e($comment); + $reply->rendered = $autolink; + $reply->in_reply_to_id = $status->id; + $reply->in_reply_to_profile_id = $status->profile_id; + $reply->save(); + + $status->reply_count++; + $status->save(); + + return $reply; + }); NewStatusPipeline::dispatch($reply, false); CommentPipeline::dispatch($status, $reply); diff --git a/app/Http/Controllers/Import/Instagram.php b/app/Http/Controllers/Import/Instagram.php index 0b1486f31..936f10b4c 100644 --- a/app/Http/Controllers/Import/Instagram.php +++ b/app/Http/Controllers/Import/Instagram.php @@ -82,9 +82,10 @@ trait Instagram ->whereStage(1) ->firstOrFail(); + $limit = config('pixelfed.import.instagram.limits.posts'); foreach ($media as $k => $v) { $original = $v->getClientOriginalName(); - if(strlen($original) < 32 || $k > 100) { + if(strlen($original) < 32 || $k > $limit) { continue; } $storagePath = "import/{$job->uuid}"; @@ -105,7 +106,6 @@ trait Instagram $job->save(); }); return redirect($job->url()); - return view('settings.import.instagram.step-one', compact('profile', 'job')); } public function instagramStepTwo(Request $request, $uuid) @@ -148,6 +148,7 @@ trait Instagram { $profile = Auth::user()->profile; $job = ImportJob::whereProfileId($profile->id) + ->whereService('instagram') ->whereNull('completed_at') ->whereUuid($uuid) ->whereStage(3) @@ -159,14 +160,21 @@ trait Instagram { $profile = Auth::user()->profile; - $job = ImportJob::whereProfileId($profile->id) + + try { + $import = ImportJob::whereProfileId($profile->id) + ->where('uuid', $uuid) + ->whereNotNull('media_json') ->whereNull('completed_at') - ->whereUuid($uuid) ->whereStage(3) ->firstOrFail(); + ImportInstagram::dispatch($import); + } catch (Exception $e) { + \Log::info($e); + } - ImportInstagram::dispatchNow($job); - - return redirect($profile->url()); + return redirect(route('settings'))->with(['status' => [ + 'Import successful! It may take a few minutes to finish.' + ]]); } } diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index 7350b2d51..2b5e0e39f 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -108,6 +108,7 @@ class PublicApiController extends Controller 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'limit' => 'nullable|integer|min:5|max:50' ]); + $limit = $request->limit ?? 10; $profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail(); $status = Status::whereProfileId($profile->id)->whereCommentsDisabled(false)->findOrFail($postId); @@ -116,7 +117,7 @@ class PublicApiController extends Controller if($request->filled('min_id')) { $replies = $status->comments() ->whereNull('reblog_of_id') - ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at') + ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->where('id', '>=', $request->min_id) ->orderBy('id', 'desc') ->paginate($limit); @@ -124,7 +125,7 @@ class PublicApiController extends Controller if($request->filled('max_id')) { $replies = $status->comments() ->whereNull('reblog_of_id') - ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at') + ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->where('id', '<=', $request->max_id) ->orderBy('id', 'desc') ->paginate($limit); @@ -132,7 +133,7 @@ class PublicApiController extends Controller } else { $replies = $status->comments() ->whereNull('reblog_of_id') - ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'created_at') + ->select('id', 'caption', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->orderBy('id', 'desc') ->paginate($limit); } @@ -180,8 +181,8 @@ class PublicApiController extends Controller if(!$user) { abort(403); } else { - $follows = $profile->followedBy(Auth::user()->profile); - if($follows == false && $profile->id !== $user->profile->id) { + $follows = $profile->followedBy($user->profile); + if($follows == false && $profile->id !== $user->profile->id && $user->is_admin == false) { abort(404); } } @@ -357,8 +358,6 @@ class PublicApiController extends Controller 'created_at', 'updated_at' )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) - ->whereLocal(true) - ->whereNull('uri') ->where('id', $dir, $id) ->whereIn('profile_id', $following) ->whereNotIn('profile_id', $filtered) @@ -386,8 +385,6 @@ class PublicApiController extends Controller 'created_at', 'updated_at' )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) - ->whereLocal(true) - ->whereNull('uri') ->whereIn('profile_id', $following) ->whereNotIn('profile_id', $filtered) ->whereNull('in_reply_to_id') @@ -453,14 +450,18 @@ class PublicApiController extends Controller 'is_nsfw', 'scope', 'local', + 'reply_count', + 'comments_disabled', 'created_at', 'updated_at' )->where('id', $dir, $id) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) ->whereNotIn('profile_id', $filtered) + ->whereNotNull('uri') ->whereNull('in_reply_to_id') ->whereNull('reblog_of_id') ->whereVisibility('public') - ->orderBy('created_at', 'desc') + ->latest() ->limit($limit) ->get(); } else { @@ -476,14 +477,17 @@ class PublicApiController extends Controller 'is_nsfw', 'scope', 'local', + 'reply_count', + 'comments_disabled', 'created_at', 'updated_at' )->whereIn('type', ['photo', 'photo:album', 'video', 'video:album']) ->whereNotIn('profile_id', $filtered) ->whereNull('in_reply_to_id') ->whereNull('reblog_of_id') + ->whereNotNull('uri') ->whereVisibility('public') - ->orderBy('created_at', 'desc') + ->latest() ->simplePaginate($limit); } @@ -524,8 +528,8 @@ class PublicApiController extends Controller { abort_unless(Auth::check(), 403); $profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id); - if($profile->is_private || !$profile->user->settings->show_profile_followers) { - return []; + if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) { + return response()->json([]); } $followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10); $resource = new Fractal\Resource\Collection($followers, new AccountTransformer()); @@ -538,8 +542,8 @@ class PublicApiController extends Controller { abort_unless(Auth::check(), 403); $profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id); - if($profile->is_private || !$profile->user->settings->show_profile_following) { - return []; + if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_following) { + return response()->json([]); } $following = $profile->following()->orderByDesc('followers.created_at')->paginate(10); $resource = new Fractal\Resource\Collection($following, new AccountTransformer()); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index b37ba5815..4bf750591 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -9,6 +9,7 @@ use App\Status; use Illuminate\Http\Request; use App\Util\ActivityPub\Helpers; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Str; use App\Transformer\Api\{ AccountTransformer, HashtagTransformer, @@ -22,17 +23,20 @@ class SearchController extends Controller $this->middleware('auth'); } - public function searchAPI(Request $request, $tag) + public function searchAPI(Request $request) { - if(mb_strlen($tag) < 3) { - return; - } + $this->validate($request, [ + 'q' => 'required|string|min:3|max:120', + 'src' => 'required|string|in:metro', + 'v' => 'required|integer|in:1' + ]); + $tag = $request->input('q'); $tag = e(urldecode($tag)); $hash = hash('sha256', $tag); $tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) { $tokens = []; - if(Helpers::validateUrl($tag) != false) { + if(Helpers::validateUrl($tag) != false && config('pixelfed.activitypub_enabled') == true && config('pixelfed.remote_follow_enabled') == true) { $remote = Helpers::fetchFromUrl($tag); if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) { $type = $remote['type']; @@ -65,7 +69,12 @@ class SearchController extends Controller } } } - $hashtags = Hashtag::select('id', 'name', 'slug')->where('slug', 'like', '%'.$tag.'%')->whereHas('posts')->limit(20)->get(); + $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag; + $hashtags = Hashtag::select('id', 'name', 'slug') + ->where('slug', 'like', '%'.$htag.'%') + ->whereHas('posts') + ->limit(20) + ->get(); if($hashtags->count() > 0) { $tags = $hashtags->map(function ($item, $key) { return [ @@ -83,9 +92,9 @@ class SearchController extends Controller }); $users = Profile::select('username', 'name', 'id') ->whereNull('status') + ->whereNull('domain') ->where('id', '!=', Auth::user()->profile->id) ->where('username', 'like', '%'.$tag.'%') - ->whereNull('domain') //->orWhere('remote_url', $tag) ->limit(20) ->get(); @@ -120,7 +129,6 @@ class SearchController extends Controller ->whereNull('reblog_of_id') ->whereProfileId(Auth::user()->profile->id) ->where('caption', 'like', '%'.$tag.'%') - ->orWhere('uri', $tag) ->latest() ->limit(10) ->get(); diff --git a/app/Http/Controllers/Settings/HomeSettings.php b/app/Http/Controllers/Settings/HomeSettings.php index c4cfc944b..f99d92ce0 100644 --- a/app/Http/Controllers/Settings/HomeSettings.php +++ b/app/Http/Controllers/Settings/HomeSettings.php @@ -47,6 +47,10 @@ trait HomeSettings $email = $request->input('email'); $user = Auth::user(); $profile = $user->profile; + $layout = $request->input('profile_layout'); + if($layout) { + $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; + } $validate = config('pixelfed.enforce_email_verification'); @@ -89,6 +93,11 @@ trait HomeSettings $changes = true; $profile->bio = $bio; } + + if ($profile->profile_layout != $layout) { + $changes = true; + $profile->profile_layout = $layout; + } } if ($changes === true) { diff --git a/app/Http/Controllers/Settings/SecuritySettings.php b/app/Http/Controllers/Settings/SecuritySettings.php index 5d1c49ad3..8cb8261e7 100644 --- a/app/Http/Controllers/Settings/SecuritySettings.php +++ b/app/Http/Controllers/Settings/SecuritySettings.php @@ -8,6 +8,7 @@ use App\Media; use App\Profile; use App\User; use App\UserFilter; +use App\UserDevice; use App\Util\Lexer\PrettyNumber; use Auth; use DB; @@ -20,19 +21,19 @@ trait SecuritySettings public function security() { - $sessions = DB::table('sessions') - ->whereUserId(Auth::id()) - ->limit(20) - ->get(); + $user = Auth::user(); - $activity = AccountLog::whereUserId(Auth::id()) + $activity = AccountLog::whereUserId($user->id) ->orderBy('created_at', 'desc') ->limit(20) ->get(); - $user = Auth::user(); + $devices = UserDevice::whereUserId($user->id) + ->orderBy('created_at', 'desc') + ->limit(5) + ->get(); - return view('settings.security', compact('sessions', 'activity', 'user')); + return view('settings.security', compact('activity', 'user', 'devices')); } public function securityTwoFactorSetup(Request $request) diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index ad477fd26..c6e38754b 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -42,11 +42,11 @@ class StatusController extends Controller if($status->visibility == 'private' || $user->is_private) { if(!Auth::check()) { - abort(403); + abort(404); } $pid = Auth::user()->profile; - if($user->followedBy($pid) == false && $user->id !== $pid->id) { - abort(403); + if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) { + abort(404); } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 968d43ed1..968c4cfa6 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -25,8 +25,21 @@ class AuthServiceProvider extends ServiceProvider { $this->registerPolicies(); - // Passport::routes(); - // Passport::tokensExpireIn(now()->addDays(15)); - // Passport::refreshTokensExpireIn(now()->addDays(30)); + if(config('pixelfed.oauth_enabled')) { + Passport::routes(); + Passport::tokensExpireIn(now()->addDays(15)); + Passport::refreshTokensExpireIn(now()->addDays(30)); + Passport::enableImplicitGrant(); + + Passport::setDefaultScope([ + 'user:read', + 'user:write' + ]); + + Passport::tokensCan([ + 'user:read' => 'Read a user’s profile info and media', + 'user:write' => 'This scope lets an app "Change your profile information"', + ]); + } } } diff --git a/app/Transformer/ActivityPub/Verb/CreateNote.php b/app/Transformer/ActivityPub/Verb/CreateNote.php index c30a2cb8c..b5f51d769 100644 --- a/app/Transformer/ActivityPub/Verb/CreateNote.php +++ b/app/Transformer/ActivityPub/Verb/CreateNote.php @@ -35,6 +35,11 @@ class CreateNote extends Fractal\TransformerAbstract 'Hashtag' => 'as:Hashtag', 'sensitive' => 'as:sensitive', 'commentsEnabled' => 'sc:Boolean', + 'capabilities' => [ + 'announce' => ['@type' => '@id'], + 'like' => ['@type' => '@id'], + 'reply' => ['@type' => '@id'] + ] ] ], 'id' => $status->permalink(), @@ -65,6 +70,11 @@ class CreateNote extends Fractal\TransformerAbstract })->toArray(), 'tag' => $tags, 'commentsEnabled' => (bool) !$status->comments_disabled, + 'capabilities' => [ + 'announce' => 'https://www.w3.org/ns/activitystreams#Public', + 'like' => 'https://www.w3.org/ns/activitystreams#Public', + 'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public' + ] ] ]; } diff --git a/app/Transformer/ActivityPub/Verb/Note.php b/app/Transformer/ActivityPub/Verb/Note.php index 3dafe32aa..ac4567179 100644 --- a/app/Transformer/ActivityPub/Verb/Note.php +++ b/app/Transformer/ActivityPub/Verb/Note.php @@ -35,6 +35,11 @@ class Note extends Fractal\TransformerAbstract 'Hashtag' => 'as:Hashtag', 'sensitive' => 'as:sensitive', 'commentsEnabled' => 'sc:Boolean', + 'capabilities' => [ + 'announce' => ['@type' => '@id'], + 'like' => ['@type' => '@id'], + 'reply' => ['@type' => '@id'], + ] ] ], 'id' => $status->url(), @@ -58,6 +63,11 @@ class Note extends Fractal\TransformerAbstract })->toArray(), 'tag' => $tags, 'commentsEnabled' => (bool) !$status->comments_disabled, + 'capabilities' => [ + 'announce' => 'https://www.w3.org/ns/activitystreams#Public', + 'like' => 'https://www.w3.org/ns/activitystreams#Public', + 'reply' => $status->comments_disabled == true ? null : 'https://www.w3.org/ns/activitystreams#Public' + ] ]; } } diff --git a/app/Transformer/Api/RelationshipTransformer.php b/app/Transformer/Api/RelationshipTransformer.php index 6c1a6bfbd..544a9bfd4 100644 --- a/app/Transformer/Api/RelationshipTransformer.php +++ b/app/Transformer/Api/RelationshipTransformer.php @@ -15,8 +15,8 @@ class RelationshipTransformer extends Fractal\TransformerAbstract 'id' => (string) $profile->id, 'following' => $user->follows($profile), 'followed_by' => $user->followedBy($profile), - 'blocking' => null, - 'muting' => null, + 'blocking' => $user->blockedIds()->contains($profile->id), + 'muting' => $user->mutedIds()->contains($profile->id), 'muting_notifications' => null, 'requested' => null, 'domain_blocking' => null, diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php index e99768155..8f6c5c837 100644 --- a/app/Transformer/Api/StatusTransformer.php +++ b/app/Transformer/Api/StatusTransformer.php @@ -23,7 +23,7 @@ class StatusTransformer extends Fractal\TransformerAbstract 'url' => $status->url(), 'in_reply_to_id' => $status->in_reply_to_id, 'in_reply_to_account_id' => $status->in_reply_to_profile_id, - 'reblog' => $status->reblog_of_id || $status->in_reply_to_id ? $this->transform($status->parent()) : null, + 'reblog' => null, 'content' => $status->rendered ?? $status->caption, 'created_at' => $status->created_at->format('c'), 'emojis' => [], @@ -42,9 +42,11 @@ class StatusTransformer extends Fractal\TransformerAbstract 'language' => null, 'pinned' => null, - 'pf_type' => $status->type ?? $status->setType(), - 'reply_count' => $status->reply_count, - 'comments_disabled' => $status->comments_disabled ? true : false + 'pf_type' => $status->type ?? $status->setType(), + 'reply_count' => (int) $status->reply_count, + 'comments_disabled' => $status->comments_disabled ? true : false, + 'thread' => false, + 'replies' => [] ]; } diff --git a/app/UserDevice.php b/app/UserDevice.php index 0ef037a35..58e29d49c 100644 --- a/app/UserDevice.php +++ b/app/UserDevice.php @@ -3,6 +3,7 @@ namespace App; use Illuminate\Database\Eloquent\Model; +use Jenssegers\Agent\Agent; class UserDevice extends Model { @@ -20,4 +21,14 @@ class UserDevice extends Model { return $this->belongsTo(User::class); } + + public function getUserAgent() + { + if(!$this->user_agent) { + return 'Unknown'; + } + $agent = new Agent(); + $agent->setUserAgent($this->user_agent); + return $agent; + } } diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index 3be0492fa..2013036ba 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -21,8 +21,6 @@ use App\Jobs\AvatarPipeline\CreateAvatar; use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent; use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail}; use App\Jobs\StatusPipeline\NewStatusPipeline; -use App\Util\HttpSignatures\{GuzzleHttpSignatures, KeyStore, Context, Verifier}; -use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; use App\Util\ActivityPub\HttpSignature; use Illuminate\Support\Str; @@ -30,7 +28,7 @@ class Helpers { public static function validateObject($data) { - $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo']; + $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone']; $valid = Validator::make($data, [ 'type' => [ @@ -38,11 +36,11 @@ class Helpers { Rule::in($verbs) ], 'id' => 'required|string', - 'actor' => 'required|string', + 'actor' => 'required|string|url', 'object' => 'required', 'object.type' => 'required_if:type,Create', 'object.attachment' => 'required_if:type,Create', - 'object.attributedTo' => 'required_if:type,Create', + 'object.attributedTo' => 'required_if:type,Create|url', 'published' => 'required_if:type,Create|date' ])->passes(); @@ -71,7 +69,7 @@ class Helpers { 'string', Rule::in($mediaTypes) ], - '*.url' => 'required|max:255', + '*.url' => 'required|url|max:255', '*.mediaType' => [ 'required', 'string', @@ -193,6 +191,7 @@ class Helpers { $res = Zttp::withHeaders(self::zttpUserAgent())->get($url); $res = json_decode($res->body(), true, 8); if(json_last_error() == JSON_ERROR_NONE) { + abort_if(!self::validateObject($res), 422); return $res; } else { return false; @@ -238,14 +237,26 @@ class Helpers { } $scope = 'private'; + $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false; - if(isset($res['to']) == true && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) { - $scope = 'public'; + if(isset($res['to']) == true) { + if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) { + $scope = 'public'; + } + if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) { + $scope = 'public'; + } } - if(isset($res['cc']) == true && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) { + if(isset($res['cc']) == true) { $scope = 'unlisted'; + if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) { + $scope = 'unlisted'; + } + if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) { + $scope = 'unlisted'; + } } if(config('costar.enabled') == true) { @@ -309,7 +320,7 @@ class Helpers { $status->scope = $scope; $status->visibility = $scope; $status->save(); - self::importNoteAttachment($res, $status); + // self::importNoteAttachment($res, $status); return $status; }); @@ -320,6 +331,8 @@ class Helpers { public static function importNoteAttachment($data, Status $status) { + return; + if(self::verifyAttachments($data) == false) { return; } @@ -336,28 +349,28 @@ class Helpers { if(in_array($type, $allowed) == false || $valid == false) { continue; } - $info = pathinfo($url); + // $info = pathinfo($url); - // pleroma attachment fix - $url = str_replace(' ', '%20', $url); + // // pleroma attachment fix + // $url = str_replace(' ', '%20', $url); - $img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]])); - $file = '/tmp/'.str_random(32); - file_put_contents($file, $img); - $fdata = new File($file); - $path = Storage::putFile($storagePath, $fdata, 'public'); - $media = new Media(); - $media->status_id = $status->id; - $media->profile_id = $status->profile_id; - $media->user_id = null; - $media->media_path = $path; - $media->size = $fdata->getSize(); - $media->mime = $fdata->getMimeType(); - $media->save(); + // $img = file_get_contents($url, false, stream_context_create(['ssl' => ["verify_peer"=>true,"verify_peer_name"=>true]])); + // $file = '/tmp/'.str_random(32); + // file_put_contents($file, $img); + // $fdata = new File($file); + // $path = Storage::putFile($storagePath, $fdata, 'public'); + // $media = new Media(); + // $media->status_id = $status->id; + // $media->profile_id = $status->profile_id; + // $media->user_id = null; + // $media->media_path = $path; + // $media->size = $fdata->getSize(); + // $media->mime = $fdata->getMimeType(); + // $media->save(); - ImageThumbnail::dispatch($media); - ImageOptimize::dispatch($media); - unlink($file); + // ImageThumbnail::dispatch($media); + // ImageOptimize::dispatch($media); + // unlink($file); } return; } @@ -380,15 +393,19 @@ class Helpers { return; } $domain = parse_url($res['id'], PHP_URL_HOST); - $username = $res['preferredUsername']; + $username = Purify::clean($res['preferredUsername']); $remoteUsername = "@{$username}@{$domain}"; + abort_if(!self::validateUrl($res['inbox']), 400); + abort_if(!self::validateUrl($res['outbox']), 400); + abort_if(!self::validateUrl($res['id']), 400); + $profile = Profile::whereRemoteUrl($res['id'])->first(); if(!$profile) { $profile = new Profile; $profile->domain = $domain; - $profile->username = $remoteUsername; - $profile->name = strip_tags($res['name']); + $profile->username = Purify::clean($remoteUsername); + $profile->name = Purify::clean($res['name']) ?? 'user'; $profile->bio = Purify::clean($res['summary']); $profile->sharedInbox = isset($res['endpoints']) && isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : null; $profile->inbox_url = $res['inbox']; @@ -407,6 +424,8 @@ class Helpers { public static function sendSignedObject($senderProfile, $url, $body) { + abort_if(!self::validateUrl($url), 400); + $payload = json_encode($body); $headers = HttpSignature::sign($senderProfile, $url, $body); @@ -418,42 +437,4 @@ class Helpers { $response = curl_exec($ch); return; } - - private static function _headersToSigningString($headers) { - } - - public static function validateSignature($request, $payload = null) - { - - } - - public static function fetchPublicKey() - { - $profile = $this->profile; - $is_url = $this->is_url; - $valid = $this->validateUrl(); - if (!$valid) { - throw new \Exception('Invalid URL provided'); - } - if ($is_url && isset($profile->public_key) && $profile->public_key) { - return $profile->public_key; - } - - try { - $url = $this->profile; - $res = Zttp::timeout(30)->withHeaders([ - 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org', - ])->get($url); - $actor = json_decode($res->getBody(), true); - } catch (Exception $e) { - throw new Exception('Unable to fetch public key'); - } - if($actor['publicKey']['owner'] != $profile) { - throw new Exception('Invalid key match'); - } - $this->public_key = $actor['publicKey']['publicKeyPem']; - $this->key_id = $actor['publicKey']['id']; - return $this; - } } \ No newline at end of file diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 47820ca90..05da7cda2 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -36,6 +36,7 @@ class Inbox public function handle() { + abort_if(!Helpers::validateObject($this->payload), 400); $this->handleVerb(); } @@ -135,6 +136,8 @@ class Inbox public function handleNoteCreate() { + return; + $activity = $this->payload['object']; $actor = $this->actorFirstOrCreate($this->payload['actor']); if(!$actor || $actor->domain == null) { @@ -259,24 +262,24 @@ class Inbox { $actor = $this->payload['actor']; $obj = $this->payload['object']; + abort_if(!Helpers::validateUrl($obj), 400); if(is_string($obj) && Helpers::validateUrl($obj)) { // actor object detected // todo delete actor - } else if (Helpers::validateUrl($obj['id']) && is_array($obj) && isset($obj['type']) && $obj['type'] == 'Tombstone') { - // tombstone detected - $status = Status::whereLocal(false)->whereUri($obj['id'])->firstOrFail(); - $status->forceDelete(); + } else if (Helpers::validateUrl($obj['id']) && Helpers::validateObject($obj) && $obj['type'] == 'Tombstone') { + // todo delete status or object } } public function handleLikeActivity() { $actor = $this->payload['actor']; + + abort_if(!Helpers::validateUrl($actor), 400); + $profile = self::actorFirstOrCreate($actor); $obj = $this->payload['object']; - if(Helpers::validateLocalUrl($obj) == false) { - return; - } + abort_if(!Helpers::validateLocalUrl($obj), 400); $status = Helpers::statusFirstOrFetch($obj); if(!$status || !$profile) { return; @@ -286,10 +289,11 @@ class Inbox 'status_id' => $status->id ]); - if($like->wasRecentlyCreated == false) { - return; + if($like->wasRecentlyCreated == true) { + LikePipeline::dispatch($like); } - LikePipeline::dispatch($like); + + return; } diff --git a/composer.json b/composer.json index 6a3cd9451..7b2b87284 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "fideloper/proxy": "^4.0", "greggilbert/recaptcha": "dev-master", "intervention/image": "^2.4", + "jenssegers/agent": "^2.6", "laravel/framework": "5.8.*", "laravel/horizon": "^3.0", "laravel/passport": "^7.0", diff --git a/composer.lock b/composer.lock index 841418418..f8741864b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "188c87638a863fd575f41213e72976f5", + "content-hash": "702a3ed0b8499d50323723eb4fb41965", "packages": [ { "name": "alchemy/binary-driver", @@ -1558,6 +1558,124 @@ "description": "Highlight PHP code in terminal", "time": "2018-09-29T18:48:56+00:00" }, + { + "name": "jaybizzle/crawler-detect", + "version": "v1.2.80", + "source": { + "type": "git", + "url": "https://github.com/JayBizzle/Crawler-Detect.git", + "reference": "af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847", + "reference": "af6a36e6d69670df3f0a3ed8e21d4b8cc67a7847", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.5|^6.5", + "satooshi/php-coveralls": "1.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jaybizzle\\CrawlerDetect\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Beech", + "email": "m@rkbee.ch", + "role": "Developer" + } + ], + "description": "CrawlerDetect is a PHP class for detecting bots/crawlers/spiders via the user agent", + "homepage": "https://github.com/JayBizzle/Crawler-Detect/", + "keywords": [ + "crawler", + "crawler detect", + "crawler detector", + "crawlerdetect", + "php crawler detect" + ], + "time": "2019-04-05T19:52:02+00:00" + }, + { + "name": "jenssegers/agent", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/jenssegers/agent.git", + "reference": "bcb895395e460478e101f41cdab139c48dc721ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jenssegers/agent/zipball/bcb895395e460478e101f41cdab139c48dc721ce", + "reference": "bcb895395e460478e101f41cdab139c48dc721ce", + "shasum": "" + }, + "require": { + "jaybizzle/crawler-detect": "^1.2", + "mobiledetect/mobiledetectlib": "^2.7.6", + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5.0|^6.0|^7.0" + }, + "suggest": { + "illuminate/support": "^4.0|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + }, + "laravel": { + "providers": [ + "Jenssegers\\Agent\\AgentServiceProvider" + ], + "aliases": { + "Agent": "Jenssegers\\Agent\\Facades\\Agent" + } + } + }, + "autoload": { + "psr-4": { + "Jenssegers\\Agent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jens Segers", + "homepage": "https://jenssegers.com" + } + ], + "description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect", + "homepage": "https://github.com/jenssegers/agent", + "keywords": [ + "Agent", + "browser", + "desktop", + "laravel", + "mobile", + "platform", + "user agent", + "useragent" + ], + "time": "2019-01-19T21:32:55+00:00" + }, { "name": "laravel/framework", "version": "v5.8.10", @@ -2270,6 +2388,58 @@ ], "time": "2019-03-29T18:19:35+00:00" }, + { + "name": "mobiledetect/mobiledetectlib", + "version": "2.8.33", + "source": { + "type": "git", + "url": "https://github.com/serbanghita/Mobile-Detect.git", + "reference": "cd385290f9a0d609d2eddd165a1e44ec1bf12102" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/cd385290f9a0d609d2eddd165a1e44ec1bf12102", + "reference": "cd385290f9a0d609d2eddd165a1e44ec1bf12102", + "shasum": "" + }, + "require": { + "php": ">=5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35||~5.7" + }, + "type": "library", + "autoload": { + "classmap": [ + "Mobile_Detect.php" + ], + "psr-0": { + "Detection": "namespaced/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Serban Ghita", + "email": "serbanghita@gmail.com", + "homepage": "http://mobiledetect.net", + "role": "Developer" + } + ], + "description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.", + "homepage": "https://github.com/serbanghita/Mobile-Detect", + "keywords": [ + "detect mobile devices", + "mobile", + "mobile detect", + "mobile detector", + "php mobile detect" + ], + "time": "2018-09-01T15:05:15+00:00" + }, { "name": "monolog/monolog", "version": "1.24.0", diff --git a/config/pixelfed.php b/config/pixelfed.php index c72004624..f1111e843 100644 --- a/config/pixelfed.php +++ b/config/pixelfed.php @@ -23,7 +23,7 @@ return [ | This value is the version of your PixelFed instance. | */ - 'version' => '0.8.6', + 'version' => '0.9.0', /* |-------------------------------------------------------------------------- @@ -46,7 +46,7 @@ return [ | default memory_limit php.ini is used for the rest of the app. | */ - 'memory_limit' => '1024M', + 'memory_limit' => env('MEMORY_LIMIT', '1024M'), /* |-------------------------------------------------------------------------- @@ -259,7 +259,9 @@ return [ 'media_types' => env('MEDIA_TYPES', 'image/jpeg,image/png,image/gif'), + 'enforce_account_limit' => env('LIMIT_ACCOUNT_SIZE', true), + 'ap_inbox' => env('ACTIVITYPUB_INBOX', false), 'ap_shared' => env('ACTIVITYPUB_SHAREDINBOX', false), 'ap_delivery_timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 2.0), @@ -267,11 +269,13 @@ return [ 'import' => [ 'instagram' => [ - 'enabled' => env('IMPORT_INSTAGRAM_ENABLED', false), + 'enabled' => false, 'limits' => [ 'posts' => (int) env('IMPORT_INSTAGRAM_POST_LIMIT', 100), 'size' => (int) env('IMPORT_INSTAGRAM_SIZE_LIMIT', 250) ] ] ], + + 'oauth_enabled' => env('OAUTH_ENABLED', false), ]; diff --git a/database/migrations/2019_04_16_184644_add_layout_to_profiles_table.php b/database/migrations/2019_04_16_184644_add_layout_to_profiles_table.php new file mode 100644 index 000000000..b0631bed3 --- /dev/null +++ b/database/migrations/2019_04_16_184644_add_layout_to_profiles_table.php @@ -0,0 +1,39 @@ +getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); + } + + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::table('profiles', function (Blueprint $table) { + $table->string('profile_layout')->nullable()->after('website'); + $table->string('post_layout')->nullable()->after('profile_layout'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('profiles', function (Blueprint $table) { + $table->dropColumn('profile_layout'); + $table->dropColumn('post_layout'); + }); + } +} diff --git a/public/js/components.js b/public/js/components.js index 978c232e3..a9a504c28 100644 Binary files a/public/js/components.js and b/public/js/components.js differ diff --git a/public/js/compose.js b/public/js/compose.js index a41c56f11..5e0e97612 100644 Binary files a/public/js/compose.js and b/public/js/compose.js differ diff --git a/public/js/developers.js b/public/js/developers.js new file mode 100644 index 000000000..06caeb981 Binary files /dev/null and b/public/js/developers.js differ diff --git a/public/js/profile.js b/public/js/profile.js index ce3af0176..765e7b131 100644 Binary files a/public/js/profile.js and b/public/js/profile.js differ diff --git a/public/js/search.js b/public/js/search.js index 3c10b39db..e48e2e11b 100644 Binary files a/public/js/search.js and b/public/js/search.js differ diff --git a/public/js/status.js b/public/js/status.js index fd62b4937..060e9832d 100644 Binary files a/public/js/status.js and b/public/js/status.js differ diff --git a/public/js/timeline.js b/public/js/timeline.js index 6d0f29de2..3cca9cf78 100644 Binary files a/public/js/timeline.js and b/public/js/timeline.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 0d97ebae5..f2d2b5672 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/assets/js/components.js b/resources/assets/js/components.js index b530ad7f1..63d25e94e 100644 --- a/resources/assets/js/components.js +++ b/resources/assets/js/components.js @@ -18,10 +18,10 @@ pixelfed.readmore = () => { return; } el.readmore({ - collapsedHeight: 44, - heightMargin: 20, - moreLink: 'Read more', - lessLink: 'Hide', + collapsedHeight: 45, + heightMargin: 48, + moreLink: 'Read more ...', + lessLink: 'Hide', }); }); }; diff --git a/resources/assets/js/components/ComposeModal.vue b/resources/assets/js/components/ComposeModal.vue index 71352fe6d..0424895cc 100644 --- a/resources/assets/js/components/ComposeModal.vue +++ b/resources/assets/js/components/ComposeModal.vue @@ -25,43 +25,56 @@
-
-

Click here to add photos.

+
+
+
+ +

Uploading ... ({{uploadProgress}}%)

+
+
-
- - - -
- -
-
-
-
-
- + + +
+
+ +
@@ -84,24 +97,13 @@
-
+
-

- -

-
-
-
-
-

- - Draft - -

+
-
+
@@ -234,7 +239,9 @@ export default { carouselCursor: 0, visibility: 'public', mediaDrawer: false, - composeState: 'publish' + composeState: 'publish', + uploading: false, + uploadProgress: 0 } }, @@ -301,6 +308,9 @@ export default { fetchProfile() { axios.get('/api/v1/accounts/verify_credentials').then(res => { this.profile = res.data; + if(res.data.locked == true) { + this.visibility = 'private'; + } }).catch(err => { console.log(err) }); @@ -320,6 +330,7 @@ export default { $(document).on('change', '.file-input', function(e) { let io = document.querySelector('.file-input'); Array.prototype.forEach.call(io.files, function(io, i) { + self.uploading = true; if(self.media && self.media.length + i >= self.config.uploader.album_limit) { swal('Error', 'You can only upload ' + self.config.uploader.album_limit + ' photos per album', 'error'); return; @@ -338,20 +349,25 @@ export default { let xhrConfig = { onUploadProgress: function(e) { let progress = Math.round( (e.loaded * 100) / e.total ); + self.uploadProgress = progress; } }; axios.post('/api/v1/media', form, xhrConfig) .then(function(e) { + self.uploadProgress = 100; self.ids.push(e.data.id); self.media.push(e.data); setTimeout(function() { - self.mediaDrawer = true; + self.uploading = false; }, 1000); }).catch(function(e) { + self.uploading = false; + io.value = null; swal('Oops, something went wrong!', 'An unexpected error occurred.', 'error'); }); io.value = null; + self.uploadProgress = 0; }); }); }, diff --git a/resources/assets/js/components/PostComponent.vue b/resources/assets/js/components/PostComponent.vue index e30c80744..13725a547 100644 --- a/resources/assets/js/components/PostComponent.vue +++ b/resources/assets/js/components/PostComponent.vue @@ -1,195 +1,285 @@