Merge pull request #2753 from pixelfed/staging

Staging
This commit is contained in:
daniel 2021-05-11 23:40:57 -06:00 committed by GitHub
commit 70e3fed0f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 2415 additions and 1822 deletions

View file

@ -4,7 +4,9 @@
### Added ### Added
- Autocomplete Support (hashtags + mentions) ([de514f7d](https://github.com/pixelfed/pixelfed/commit/de514f7d)) - Autocomplete Support (hashtags + mentions) ([de514f7d](https://github.com/pixelfed/pixelfed/commit/de514f7d))
- Creative Commons Licenses ([552e950](https://github.com/pixelfed/pixelfed/commit/552e950)) - Creative Commons Licenses ([552e950](https://github.com/pixelfed/pixelfed/commit/552e950))
- Add Network Timeline ([af7face4](https://github.com/pixelfed/pixelfed/commit/af7face4)) - Network Timeline ([af7face4](https://github.com/pixelfed/pixelfed/commit/af7face4))
- Admin config settings ([f2066b74](https://github.com/pixelfed/pixelfed/commit/f2066b74))
- Profile pronouns ([fabb57a9](https://github.com/pixelfed/pixelfed/commit/fabb57a9))
### Updated ### Updated
- Updated AdminController, fix variable name in updateSpam method. ([6edaf940](https://github.com/pixelfed/pixelfed/commit/6edaf940)) - Updated AdminController, fix variable name in updateSpam method. ([6edaf940](https://github.com/pixelfed/pixelfed/commit/6edaf940))
@ -83,6 +85,7 @@
- Updated PostComponent, change like logic. ([0a35f5d6](https://github.com/pixelfed/pixelfed/commit/0a35f5d6)) - Updated PostComponent, change like logic. ([0a35f5d6](https://github.com/pixelfed/pixelfed/commit/0a35f5d6))
- Updated Timeline component, change like logic. ([7bcbf96b](https://github.com/pixelfed/pixelfed/commit/7bcbf96b)) - Updated Timeline component, change like logic. ([7bcbf96b](https://github.com/pixelfed/pixelfed/commit/7bcbf96b))
- Updated LikeService, fix likedBy method. ([a5e64da6](https://github.com/pixelfed/pixelfed/commit/a5e64da6)) - Updated LikeService, fix likedBy method. ([a5e64da6](https://github.com/pixelfed/pixelfed/commit/a5e64da6))
- Updated PublicApiController, increase public timeline to 6 months from 3. ([8a736432](https://github.com/pixelfed/pixelfed/commit/8a736432))
- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10) ## [v0.10.10 (2021-01-28)](https://github.com/pixelfed/pixelfed/compare/v0.10.9...v0.10.10)

View file

@ -8,12 +8,116 @@ use Carbon\Carbon;
use App\{Comment, Like, Media, Page, Profile, Report, Status, User}; use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Util\Lexer\PrettyNumber; use App\Util\Lexer\PrettyNumber;
use App\Models\ConfigCache;
use App\Services\ConfigCacheService;
trait AdminSettingsController trait AdminSettingsController
{ {
public function settings(Request $request) public function settings(Request $request)
{ {
return view('admin.settings.home'); $name = ConfigCacheService::get('app.name');
$short_description = ConfigCacheService::get('app.short_description');
$description = ConfigCacheService::get('app.description');
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
$jpeg = in_array('image/jpg', $types) ? true : in_array('image/jpeg', $types);
$png = in_array('image/png', $types);
$gif = in_array('image/gif', $types);
$mp4 = in_array('video/mp4', $types);
return view('admin.settings.home', compact(
'name',
'short_description',
'description',
'jpeg',
'png',
'gif',
'mp4'
));
}
public function settingsHomeStore(Request $request)
{
$this->validate($request, [
'name' => 'nullable|string',
'short_description' => 'nullable',
'long_description' => 'nullable',
'max_photo_size' => 'nullable|integer|min:1',
'max_album_length' => 'nullable|integer|min:1|max:100',
'image_quality' => 'nullable|integer|min:1|max:100',
'type_jpeg' => 'nullable',
'type_png' => 'nullable',
'type_gif' => 'nullable',
'type_mp4' => 'nullable',
]);
$media_types = explode(',', config_cache('pixelfed.media_types'));
$media_types_original = $media_types;
$mimes = [
'type_jpeg' => 'image/jpeg',
'type_png' => 'image/png',
'type_gif' => 'image/gif',
'type_mp4' => 'video/mp4',
];
foreach ($mimes as $key => $value) {
if($request->input($key) == 'on') {
if(!in_array($value, $media_types)) {
array_push($media_types, $value);
}
} else {
$media_types = array_diff($media_types, [$value]);
}
}
if($media_types !== $media_types_original) {
ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
}
$keys = [
'name' => 'app.name',
'short_description' => 'app.short_description',
'long_description' => 'app.description',
'max_photo_size' => 'pixelfed.max_photo_size',
'max_album_length' => 'pixelfed.max_album_length',
'image_quality' => 'pixelfed.image_quality',
'account_limit' => 'pixelfed.max_account_size',
'custom_css' => 'uikit.custom.css',
'custom_js' => 'uikit.custom.js'
];
foreach ($keys as $key => $value) {
$cc = ConfigCache::whereK($value)->first();
$val = $request->input($key);
if($cc && $cc->v != $val) {
ConfigCacheService::put($value, $val);
}
}
$bools = [
'activitypub' => 'federation.activitypub.enabled',
'open_registration' => 'pixelfed.open_registration',
'mobile_apis' => 'pixelfed.oauth_enabled',
'stories' => 'instance.stories.enabled',
'ig_import' => 'pixelfed.import.instagram.enabled',
'spam_detection' => 'pixelfed.bouncer.enabled',
'require_email_verification' => 'pixelfed.enforce_email_verification',
'enforce_account_limit' => 'pixelfed.enforce_account_limit',
'show_custom_css' => 'uikit.show_custom.css',
'show_custom_js' => 'uikit.show_custom.js',
];
foreach ($bools as $key => $value) {
$active = $request->input($key) == 'on';
if(config_cache($value) !== $active) {
ConfigCacheService::put($value, (bool) $active);
}
}
Cache::forget('api:site:configuration:_v0.2');
return redirect('/i/admin/settings');
} }
public function settingsBackups(Request $request) public function settingsBackups(Request $request)
@ -84,15 +188,6 @@ trait AdminSettingsController
return view('admin.settings.features'); return view('admin.settings.features');
} }
public function settingsHomeStore(Request $request)
{
$this->validate($request, [
'APP_NAME' => 'required|string',
]);
// Artisan::call('config:clear');
return redirect()->back();
}
public function settingsPages(Request $request) public function settingsPages(Request $request)
{ {
$pages = Page::orderByDesc('updated_at')->paginate(10); $pages = Page::orderByDesc('updated_at')->paginate(10);
@ -135,4 +230,4 @@ trait AdminSettingsController
} }
return view('admin.settings.system', compact('sys')); return view('admin.settings.system', compact('sys'));
} }
} }

View file

@ -66,7 +66,7 @@ class ApiV1Controller extends Controller
public function apps(Request $request) public function apps(Request $request)
{ {
abort_if(!config('pixelfed.oauth_enabled'), 404); abort_if(!config_cache('pixelfed.oauth_enabled'), 404);
$this->validate($request, [ $this->validate($request, [
'client_name' => 'required', 'client_name' => 'required',
@ -960,31 +960,31 @@ class ApiV1Controller extends Controller
$res = [ $res = [
'approval_required' => false, 'approval_required' => false,
'contact_account' => null, 'contact_account' => null,
'description' => config('instance.description'), 'description' => config_cache('app.description'),
'email' => config('instance.email'), 'email' => config('instance.email'),
'invites_enabled' => false, 'invites_enabled' => false,
'rules' => [], 'rules' => [],
'short_description' => 'Pixelfed - Photo sharing for everyone', 'short_description' => 'Pixelfed - Photo sharing for everyone',
'languages' => ['en'], 'languages' => ['en'],
'max_toot_chars' => (int) config('pixelfed.max_caption_length'), 'max_toot_chars' => (int) config('pixelfed.max_caption_length'),
'registrations' => config('pixelfed.open_registration'), 'registrations' => config_cache('pixelfed.open_registration'),
'stats' => [ 'stats' => [
'user_count' => 0, 'user_count' => 0,
'status_count' => 0, 'status_count' => 0,
'domain_count' => 0 'domain_count' => 0
], ],
'thumbnail' => config('app.url') . '/img/pixelfed-icon-color.png', 'thumbnail' => config('app.url') . '/img/pixelfed-icon-color.png',
'title' => config('app.name'), 'title' => config_cache('app.name'),
'uri' => config('pixelfed.domain.app'), 'uri' => config('pixelfed.domain.app'),
'urls' => [], 'urls' => [],
'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') . ')', 'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') . ')',
'environment' => [ 'environment' => [
'max_photo_size' => (int) config('pixelfed.max_photo_size'), 'max_photo_size' => (int) config_cache('pixelfed.max_photo_size'),
'max_avatar_size' => (int) config('pixelfed.max_avatar_size'), 'max_avatar_size' => (int) config('pixelfed.max_avatar_size'),
'max_caption_length' => (int) config('pixelfed.max_caption_length'), 'max_caption_length' => (int) config('pixelfed.max_caption_length'),
'max_bio_length' => (int) config('pixelfed.max_bio_length'), 'max_bio_length' => (int) config('pixelfed.max_bio_length'),
'max_album_length' => (int) config('pixelfed.max_album_length'), 'max_album_length' => (int) config_cache('pixelfed.max_album_length'),
'mobile_apis' => config('pixelfed.oauth_enabled') 'mobile_apis' => config_cache('pixelfed.oauth_enabled')
] ]
]; ];
@ -1033,8 +1033,8 @@ class ApiV1Controller extends Controller
'file.*' => function() { 'file.*' => function() {
return [ return [
'required', 'required',
'mimes:' . config('pixelfed.media_types'), 'mimes:' . config_cache('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'), 'max:' . config_cache('pixelfed.max_photo_size'),
]; ];
}, },
'filter_name' => 'nullable|string|max:24', 'filter_name' => 'nullable|string|max:24',
@ -1059,11 +1059,11 @@ class ApiV1Controller extends Controller
$profile = $user->profile; $profile = $user->profile;
if(config('pixelfed.enforce_account_limit') == true) { if(config_cache('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000; return Media::whereUserId($user->id)->sum('size') / 1000;
}); });
$limit = (int) config('pixelfed.max_account_size'); $limit = (int) config_cache('pixelfed.max_account_size');
if ($size >= $limit) { if ($size >= $limit) {
abort(403, 'Account size limit reached.'); abort(403, 'Account size limit reached.');
} }
@ -1074,7 +1074,7 @@ class ApiV1Controller extends Controller
$photo = $request->file('file'); $photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types')); $mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) { if(in_array($photo->getMimeType(), $mimes) == false) {
abort(403, 'Invalid or unsupported mime type.'); abort(403, 'Invalid or unsupported mime type.');
} }
@ -1742,7 +1742,7 @@ class ApiV1Controller extends Controller
$this->validate($request, [ $this->validate($request, [
'status' => 'nullable|string', 'status' => 'nullable|string',
'in_reply_to_id' => 'nullable|integer', 'in_reply_to_id' => 'nullable|integer',
'media_ids' => 'array|max:' . config('pixelfed.max_album_length'), 'media_ids' => 'array|max:' . config_cache('pixelfed.max_album_length'),
'media_ids.*' => 'integer|min:1', 'media_ids.*' => 'integer|min:1',
'sensitive' => 'nullable|boolean', 'sensitive' => 'nullable|boolean',
'visibility' => 'string|in:private,unlisted,public', 'visibility' => 'string|in:private,unlisted,public',
@ -1824,7 +1824,7 @@ class ApiV1Controller extends Controller
$mimes = []; $mimes = [];
foreach($ids as $k => $v) { foreach($ids as $k => $v) {
if($k + 1 > config('pixelfed.max_album_length')) { if($k + 1 > config_cache('pixelfed.max_album_length')) {
continue; continue;
} }
$m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v); $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v);

View file

@ -34,7 +34,7 @@ class InstanceApiController extends Controller {
$res = [ $res = [
'uri' => config('pixelfed.domain.app'), 'uri' => config('pixelfed.domain.app'),
'title' => config('app.name'), 'title' => config_cache('app.name'),
'description' => '', 'description' => '',
'version' => config('pixelfed.version'), 'version' => config('pixelfed.version'),
'urls' => [], 'urls' => [],

View file

@ -14,183 +14,183 @@ use App\Services\EmailService;
class RegisterController extends Controller class RegisterController extends Controller
{ {
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Register Controller | Register Controller
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This controller handles the registration of new users as well as their | This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to | validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code. | provide this functionality without requiring any additional code.
| |
*/ */
use RegistersUsers; use RegistersUsers;
/** /**
* Where to redirect users after registration. * Where to redirect users after registration.
* *
* @var string * @var string
*/ */
protected $redirectTo = '/'; protected $redirectTo = '/';
/** /**
* Create a new controller instance. * Create a new controller instance.
* *
* @return void * @return void
*/ */
public function __construct() public function __construct()
{ {
$this->middleware('guest'); $this->middleware('guest');
} }
/** /**
* Get a validator for an incoming registration request. * Get a validator for an incoming registration request.
* *
* @param array $data * @param array $data
* *
* @return \Illuminate\Contracts\Validation\Validator * @return \Illuminate\Contracts\Validation\Validator
*/ */
protected function validator(array $data) protected function validator(array $data)
{ {
if(config('database.default') == 'pgsql') { if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']); $data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']); $data['email'] = strtolower($data['email']);
} }
$usernameRules = [ $usernameRules = [
'required', 'required',
'min:2', 'min:2',
'max:15', 'max:15',
'unique:users', 'unique:users',
function ($attribute, $value, $fail) { function ($attribute, $value, $fail) {
$dash = substr_count($value, '-'); $dash = substr_count($value, '-');
$underscore = substr_count($value, '_'); $underscore = substr_count($value, '_');
$period = substr_count($value, '.'); $period = substr_count($value, '.');
if(ends_with($value, ['.php', '.js', '.css'])) { if(ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.'); return $fail('Username is invalid.');
} }
if(($dash + $underscore + $period) > 1) { if(($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
} }
if (!ctype_alpha($value[0])) { if (!ctype_alpha($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.'); return $fail('Username is invalid. Must start with a letter or number.');
} }
if (!ctype_alnum($value[strlen($value) - 1])) { if (!ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.'); return $fail('Username is invalid. Must end with a letter or number.');
} }
$val = str_replace(['_', '.', '-'], '', $value); $val = str_replace(['_', '.', '-'], '', $value);
if(!ctype_alnum($val)) { if(!ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
} }
$restricted = RestrictedNames::get(); $restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) { if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.'); return $fail('Username cannot be used.');
} }
}, },
]; ];
$emailRules = [ $emailRules = [
'required', 'required',
'string', 'string',
'email', 'email',
'max:255', 'max:255',
'unique:users', 'unique:users',
function ($attribute, $value, $fail) { function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value); $banned = EmailService::isBanned($value);
if($banned) { if($banned) {
return $fail('Email is invalid.'); return $fail('Email is invalid.');
} }
}, },
]; ];
$rules = [ $rules = [
'agecheck' => 'required|accepted', 'agecheck' => 'required|accepted',
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
'username' => $usernameRules, 'username' => $usernameRules,
'email' => $emailRules, 'email' => $emailRules,
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed', 'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
]; ];
if(config('captcha.enabled')) { if(config('captcha.enabled')) {
$rules['h-captcha-response'] = 'required|captcha'; $rules['h-captcha-response'] = 'required|captcha';
} }
return Validator::make($data, $rules); return Validator::make($data, $rules);
} }
/** /**
* Create a new user instance after a valid registration. * Create a new user instance after a valid registration.
* *
* @param array $data * @param array $data
* *
* @return \App\User * @return \App\User
*/ */
protected function create(array $data) protected function create(array $data)
{ {
if(config('database.default') == 'pgsql') { if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']); $data['username'] = strtolower($data['username']);
$data['email'] = strtolower($data['email']); $data['email'] = strtolower($data['email']);
} }
return User::create([ return User::create([
'name' => $data['name'], 'name' => $data['name'],
'username' => $data['username'], 'username' => $data['username'],
'email' => $data['email'], 'email' => $data['email'],
'password' => Hash::make($data['password']), 'password' => Hash::make($data['password']),
]); ]);
} }
/** /**
* Show the application registration form. * Show the application registration form.
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function showRegistrationForm() public function showRegistrationForm()
{ {
if(config('pixelfed.open_registration')) { if(config_cache('pixelfed.open_registration')) {
$limit = config('pixelfed.max_users'); $limit = config('pixelfed.max_users');
if($limit) { if($limit) {
abort_if($limit <= User::count(), 404); abort_if($limit <= User::count(), 404);
return view('auth.register'); return view('auth.register');
} else { } else {
return view('auth.register'); return view('auth.register');
} }
} else { } else {
abort(404); abort(404);
} }
} }
/** /**
* Handle a registration request for the application. * Handle a registration request for the application.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function register(Request $request) public function register(Request $request)
{ {
abort_if(config('pixelfed.open_registration') == false, 400); abort_if(config_cache('pixelfed.open_registration') == false, 400);
$count = User::count(); $count = User::count();
$limit = config('pixelfed.max_users'); $limit = config('pixelfed.max_users');
if(false == config('pixelfed.open_registration') || $limit && $limit <= $count) { if(false == config_cache('pixelfed.open_registration') || $limit && $limit <= $count) {
return abort(403); return abort(403);
} }
$this->validator($request->all())->validate(); $this->validator($request->all())->validate();
event(new Registered($user = $this->create($request->all()))); event(new Registered($user = $this->create($request->all())));
$this->guard()->login($user); $this->guard()->login($user);
return $this->registered($request, $user) return $this->registered($request, $user)
?: redirect($this->redirectPath()); ?: redirect($this->redirectPath());
} }
} }

View file

@ -71,8 +71,8 @@ class ComposeController extends Controller
'file.*' => function() { 'file.*' => function() {
return [ return [
'required', 'required',
'mimes:' . config('pixelfed.media_types'), 'mimes:' . config_cache('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'), 'max:' . config_cache('pixelfed.max_photo_size'),
]; ];
}, },
'filter_name' => 'nullable|string|max:24', 'filter_name' => 'nullable|string|max:24',
@ -92,11 +92,11 @@ class ComposeController extends Controller
abort_if($limitReached == true, 429); abort_if($limitReached == true, 429);
if(config('pixelfed.enforce_account_limit') == true) { if(config_cache('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000; return Media::whereUserId($user->id)->sum('size') / 1000;
}); });
$limit = (int) config('pixelfed.max_account_size'); $limit = (int) config_cache('pixelfed.max_account_size');
if ($size >= $limit) { if ($size >= $limit) {
abort(403, 'Account size limit reached.'); abort(403, 'Account size limit reached.');
} }
@ -107,7 +107,7 @@ class ComposeController extends Controller
$photo = $request->file('file'); $photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types')); $mimes = explode(',', config_cache('pixelfed.media_types'));
abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format'); abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
@ -132,7 +132,7 @@ class ComposeController extends Controller
$preview_url = $media->url() . '?v=' . time(); $preview_url = $media->url() . '?v=' . time();
$url = $media->url() . '?v=' . time(); $url = $media->url() . '?v=' . time();
switch ($media->mime) { switch ($media->mime) {
case 'image/jpeg': case 'image/jpeg':
case 'image/png': case 'image/png':
@ -164,8 +164,8 @@ class ComposeController extends Controller
'file' => function() { 'file' => function() {
return [ return [
'required', 'required',
'mimes:' . config('pixelfed.media_types'), 'mimes:' . config_cache('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'), 'max:' . config_cache('pixelfed.max_photo_size'),
]; ];
}, },
]); ]);
@ -454,7 +454,7 @@ class ComposeController extends Controller
$optimize_media = (bool) $request->input('optimize_media'); $optimize_media = (bool) $request->input('optimize_media');
foreach($medias as $k => $media) { foreach($medias as $k => $media) {
if($k + 1 > config('pixelfed.max_album_length')) { if($k + 1 > config_cache('pixelfed.max_album_length')) {
continue; continue;
} }
$m = Media::findOrFail($media['id']); $m = Media::findOrFail($media['id']);
@ -648,7 +648,7 @@ class ComposeController extends Controller
case 'video/mp4': case 'video/mp4':
$finished = config('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; $finished = config('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
break; break;
default: default:
# code... # code...
break; break;

View file

@ -160,7 +160,7 @@ class DirectMessageController extends Controller
'messages' => [] 'messages' => []
]; ];
}); });
} }
} elseif(config('database.default') == 'mysql') { } elseif(config('database.default') == 'mysql') {
if($action == 'inbox') { if($action == 'inbox') {
$dms = DirectMessage::selectRaw('*, max(created_at) as createdAt') $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
@ -334,7 +334,7 @@ class DirectMessageController extends Controller
$dm->type = 'link'; $dm->type = 'link';
$dm->meta = [ $dm->meta = [
'domain' => parse_url($msg, PHP_URL_HOST), 'domain' => parse_url($msg, PHP_URL_HOST),
'local' => parse_url($msg, PHP_URL_HOST) == 'local' => parse_url($msg, PHP_URL_HOST) ==
parse_url(config('app.url'), PHP_URL_HOST) parse_url(config('app.url'), PHP_URL_HOST)
]; ];
$dm->save(); $dm->save();
@ -500,8 +500,8 @@ class DirectMessageController extends Controller
'file' => function() { 'file' => function() {
return [ return [
'required', 'required',
'mimes:' . config('pixelfed.media_types'), 'mimes:' . config_cache('pixelfed.media_types'),
'max:' . config('pixelfed.max_photo_size'), 'max:' . config_cache('pixelfed.max_photo_size'),
]; ];
}, },
'to_id' => 'required' 'to_id' => 'required'
@ -522,18 +522,18 @@ class DirectMessageController extends Controller
$hidden = false; $hidden = false;
} }
if(config('pixelfed.enforce_account_limit') == true) { if(config_cache('pixelfed.enforce_account_limit') == true) {
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) { $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
return Media::whereUserId($user->id)->sum('size') / 1000; return Media::whereUserId($user->id)->sum('size') / 1000;
}); });
$limit = (int) config('pixelfed.max_account_size'); $limit = (int) config_cache('pixelfed.max_account_size');
if ($size >= $limit) { if ($size >= $limit) {
abort(403, 'Account size limit reached.'); abort(403, 'Account size limit reached.');
} }
} }
$photo = $request->file('file'); $photo = $request->file('file');
$mimes = explode(',', config('pixelfed.media_types')); $mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), $mimes) == false) { if(in_array($photo->getMimeType(), $mimes) == false) {
abort(403, 'Invalid or unsupported mime type.'); abort(403, 'Invalid or unsupported mime type.');
} }

View file

@ -79,7 +79,7 @@ class FederationController extends Controller
public function userOutbox(Request $request, $username) public function userOutbox(Request $request, $username)
{ {
abort_if(!config('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404); abort_if(!config('federation.activitypub.outbox'), 404);
$profile = Profile::whereNull('domain') $profile = Profile::whereNull('domain')
@ -99,7 +99,7 @@ class FederationController extends Controller
public function userInbox(Request $request, $username) public function userInbox(Request $request, $username)
{ {
abort_if(!config('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.inbox'), 404); abort_if(!config('federation.activitypub.inbox'), 404);
$headers = $request->headers->all(); $headers = $request->headers->all();
@ -110,7 +110,7 @@ class FederationController extends Controller
public function sharedInbox(Request $request) public function sharedInbox(Request $request)
{ {
abort_if(!config('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.sharedInbox'), 404); abort_if(!config('federation.activitypub.sharedInbox'), 404);
$headers = $request->headers->all(); $headers = $request->headers->all();
@ -121,13 +121,13 @@ class FederationController extends Controller
public function userFollowing(Request $request, $username) public function userFollowing(Request $request, $username)
{ {
abort_if(!config('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
$profile = Profile::whereNull('remote_url') $profile = Profile::whereNull('remote_url')
->whereUsername($username) ->whereUsername($username)
->whereIsPrivate(false) ->whereIsPrivate(false)
->firstOrFail(); ->firstOrFail();
if($profile->status != null) { if($profile->status != null) {
abort(404); abort(404);
} }
@ -139,12 +139,12 @@ class FederationController extends Controller
'totalItems' => 0, 'totalItems' => 0,
'orderedItems' => [] 'orderedItems' => []
]; ];
return response()->json($obj); return response()->json($obj);
} }
public function userFollowers(Request $request, $username) public function userFollowers(Request $request, $username)
{ {
abort_if(!config('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
$profile = Profile::whereNull('remote_url') $profile = Profile::whereNull('remote_url')
->whereUsername($username) ->whereUsername($username)
@ -163,6 +163,6 @@ class FederationController extends Controller
'orderedItems' => [] 'orderedItems' => []
]; ];
return response()->json($obj); return response()->json($obj);
} }
} }

View file

@ -12,7 +12,7 @@ class ImportController extends Controller
{ {
$this->middleware('auth'); $this->middleware('auth');
if(config('pixelfed.import.instagram.enabled') != true) { if(config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled'); abort(404, 'Feature not enabled');
} }
} }

View file

@ -20,229 +20,229 @@ use App\Transformer\ActivityPub\ProfileTransformer;
class ProfileController extends Controller class ProfileController extends Controller
{ {
public function show(Request $request, $username) public function show(Request $request, $username)
{ {
$user = Profile::whereNull('domain') $user = Profile::whereNull('domain')
->whereNull('status') ->whereNull('status')
->whereUsername($username) ->whereUsername($username)
->firstOrFail(); ->firstOrFail();
if($request->wantsJson() && config('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
return $this->buildProfile($request, $user);
}
protected function buildProfile(Request $request, $user) if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
{ return $this->showActivityPub($request, $user);
$username = $user->username; }
$loggedIn = Auth::check(); return $this->buildProfile($request, $user);
$isPrivate = false; }
$isBlocked = false;
if(!$loggedIn) {
$key = 'profile:settings:' . $user->id;
$ttl = now()->addHours(6);
$settings = Cache::remember($key, $ttl, function() use($user) {
return $user->user->settings;
});
if ($user->is_private == true) { protected function buildProfile(Request $request, $user)
abort(404); {
} $username = $user->username;
$loggedIn = Auth::check();
$isPrivate = false;
$isBlocked = false;
if(!$loggedIn) {
$key = 'profile:settings:' . $user->id;
$ttl = now()->addHours(6);
$settings = Cache::remember($key, $ttl, function() use($user) {
return $user->user->settings;
});
$owner = false; if ($user->is_private == true) {
$is_following = false; abort(404);
}
$is_admin = $user->user->is_admin; $owner = false;
$profile = $user; $is_following = false;
$settings = [
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
return view($ui, compact('profile', 'settings')); $is_admin = $user->user->is_admin;
} else { $profile = $user;
$key = 'profile:settings:' . $user->id; $settings = [
$ttl = now()->addHours(6); 'crawlable' => $settings->crawlable,
$settings = Cache::remember($key, $ttl, function() use($user) { 'following' => [
return $user->user->settings; 'count' => $settings->show_profile_following_count,
}); 'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
if ($user->is_private == true) { return view($ui, compact('profile', 'settings'));
$isPrivate = $this->privateProfileCheck($user, $loggedIn); } else {
} $key = 'profile:settings:' . $user->id;
$ttl = now()->addHours(6);
$settings = Cache::remember($key, $ttl, function() use($user) {
return $user->user->settings;
});
$isBlocked = $this->blockedProfileCheck($user); if ($user->is_private == true) {
$isPrivate = $this->privateProfileCheck($user, $loggedIn);
}
$owner = $loggedIn && Auth::id() === $user->user_id; $isBlocked = $this->blockedProfileCheck($user);
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
if ($isPrivate == true || $isBlocked == true) { $owner = $loggedIn && Auth::id() === $user->user_id;
$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id) $is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
->whereFollowingId($user->id)
->exists() : false;
return view('profile.private', compact('user', 'is_following', 'requested'));
}
$is_admin = is_null($user->domain) ? $user->user->is_admin : false; if ($isPrivate == true || $isBlocked == true) {
$profile = $user; $requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
$settings = [ ->whereFollowingId($user->id)
'crawlable' => $settings->crawlable, ->exists() : false;
'following' => [ return view('profile.private', compact('user', 'is_following', 'requested'));
'count' => $settings->show_profile_following_count, }
'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
return view($ui, compact('profile', 'settings'));
}
}
public function permalinkRedirect(Request $request, $username) $is_admin = is_null($user->domain) ? $user->user->is_admin : false;
{ $profile = $user;
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); $settings = [
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers
]
];
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
return view($ui, compact('profile', 'settings'));
}
}
if ($request->wantsJson() && config('federation.activitypub.enabled')) { public function permalinkRedirect(Request $request, $username)
return $this->showActivityPub($request, $user); {
} $user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
return redirect($user->url()); if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
} return $this->showActivityPub($request, $user);
}
protected function privateProfileCheck(Profile $profile, $loggedIn) return redirect($user->url());
{ }
if (!Auth::check()) {
return true;
}
$user = Auth::user()->profile; protected function privateProfileCheck(Profile $profile, $loggedIn)
if($user->id == $profile->id || !$profile->is_private) { {
return false; if (!Auth::check()) {
} return true;
}
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists(); $user = Auth::user()->profile;
if ($follows == false) { if($user->id == $profile->id || !$profile->is_private) {
return true; return false;
} }
return false;
}
public static function accountCheck(Profile $profile) $follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
{ if ($follows == false) {
switch ($profile->status) { return true;
case 'disabled': }
case 'suspended':
case 'delete':
return view('profile.disabled');
break;
default:
break;
}
return abort(404);
}
protected function blockedProfileCheck(Profile $profile) return false;
{ }
$pid = Auth::user()->profile->id;
$blocks = UserFilter::whereUserId($profile->id)
->whereFilterType('block')
->whereFilterableType('App\Profile')
->pluck('filterable_id')
->toArray();
if (in_array($pid, $blocks)) {
return true;
}
return false; public static function accountCheck(Profile $profile)
} {
switch ($profile->status) {
case 'disabled':
case 'suspended':
case 'delete':
return view('profile.disabled');
break;
public function showActivityPub(Request $request, $user) default:
{ break;
abort_if(!config('federation.activitypub.enabled'), 404); }
abort_if($user->domain, 404); return abort(404);
}
$fractal = new Fractal\Manager(); protected function blockedProfileCheck(Profile $profile)
$resource = new Fractal\Resource\Item($user, new ProfileTransformer); {
$res = $fractal->createData($resource)->toArray(); $pid = Auth::user()->profile->id;
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); $blocks = UserFilter::whereUserId($profile->id)
} ->whereFilterType('block')
->whereFilterableType('App\Profile')
->pluck('filterable_id')
->toArray();
if (in_array($pid, $blocks)) {
return true;
}
public function showAtomFeed(Request $request, $user) return false;
{ }
abort_if(!config('federation.atom.enabled'), 404);
$profile = $user = Profile::whereNull('status')->whereNull('domain')->whereUsername($user)->whereIsPrivate(false)->firstOrFail(); public function showActivityPub(Request $request, $user)
if($profile->status != null) { {
return $this->accountCheck($profile); abort_if(!config_cache('federation.activitypub.enabled'), 404);
} abort_if($user->domain, 404);
if($profile->is_private || Auth::check()) {
$blocked = $this->blockedProfileCheck($profile);
$check = $this->privateProfileCheck($profile, null);
if($check || $blocked) {
return redirect($profile->url());
}
}
$items = $profile->statuses()->whereHas('media')->whereIn('visibility',['public', 'unlisted'])->orderBy('created_at', 'desc')->take(10)->get();
return response()->view('atom.user', compact('profile', 'items'))
->header('Content-Type', 'application/atom+xml');
}
public function meRedirect() $fractal = new Fractal\Manager();
{ $resource = new Fractal\Resource\Item($user, new ProfileTransformer);
abort_if(!Auth::check(), 404); $res = $fractal->createData($resource)->toArray();
return redirect(Auth::user()->url()); return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
} }
public function embed(Request $request, $username) public function showAtomFeed(Request $request, $user)
{ {
$res = view('profile.embed-removed'); abort_if(!config('federation.atom.enabled'), 404);
if(strlen($username) > 15 || strlen($username) < 2) { $profile = $user = Profile::whereNull('status')->whereNull('domain')->whereUsername($user)->whereIsPrivate(false)->firstOrFail();
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); if($profile->status != null) {
} return $this->accountCheck($profile);
}
if($profile->is_private || Auth::check()) {
$blocked = $this->blockedProfileCheck($profile);
$check = $this->privateProfileCheck($profile, null);
if($check || $blocked) {
return redirect($profile->url());
}
}
$items = $profile->statuses()->whereHas('media')->whereIn('visibility',['public', 'unlisted'])->orderBy('created_at', 'desc')->take(10)->get();
return response()->view('atom.user', compact('profile', 'items'))
->header('Content-Type', 'application/atom+xml');
}
$profile = Profile::whereUsername($username) public function meRedirect()
->whereIsPrivate(false) {
->whereNull('status') abort_if(!Auth::check(), 404);
->whereNull('domain') return redirect(Auth::user()->url());
->first(); }
if(!$profile) { public function embed(Request $request, $username)
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']); {
} $res = view('profile.embed-removed');
$content = Cache::remember('profile:embed:'.$profile->id, now()->addHours(12), function() use($profile) { if(strlen($username) > 15 || strlen($username) < 2) {
return View::make('profile.embed')->with(compact('profile'))->render(); return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}); }
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function stories(Request $request, $username) $profile = Profile::whereUsername($username)
{ ->whereIsPrivate(false)
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); ->whereNull('status')
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); ->whereNull('domain')
$pid = $profile->id; ->first();
$authed = Auth::user()->profile;
abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404); if(!$profile) {
$exists = Story::whereProfileId($pid) return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
->where('expires_at', '>', now()) }
->count();
abort_unless($exists > 0, 404); $content = Cache::remember('profile:embed:'.$profile->id, now()->addHours(12), function() use($profile) {
return view('profile.story', compact('pid', 'profile')); return View::make('profile.embed')->with(compact('profile'))->render();
} });
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function stories(Request $request, $username)
{
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$pid = $profile->id;
$authed = Auth::user()->profile;
abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404);
$exists = Story::whereProfileId($pid)
->where('expires_at', '>', now())
->count();
abort_unless($exists > 0, 404);
return view('profile.story', compact('pid', 'profile'));
}
} }

View file

@ -314,7 +314,7 @@ class PublicApiController extends Controller
->whereNotIn('profile_id', $filtered) ->whereNotIn('profile_id', $filtered)
->whereLocal(true) ->whereLocal(true)
->whereScope('public') ->whereScope('public')
->where('created_at', '>', now()->subMonths(3)) ->where('created_at', '>', now()->subMonths(6))
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->limit($limit) ->limit($limit)
->get(); ->get();
@ -343,7 +343,7 @@ class PublicApiController extends Controller
->with('profile', 'hashtags', 'mentions') ->with('profile', 'hashtags', 'mentions')
->whereLocal(true) ->whereLocal(true)
->whereScope('public') ->whereScope('public')
->where('created_at', '>', now()->subMonths(3)) ->where('created_at', '>', now()->subMonths(6))
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->simplePaginate($limit); ->simplePaginate($limit);
} }

View file

@ -12,348 +12,348 @@ use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Transformer\Api\{ use App\Transformer\Api\{
AccountTransformer, AccountTransformer,
HashtagTransformer, HashtagTransformer,
StatusTransformer, StatusTransformer,
}; };
use App\Services\WebfingerService; use App\Services\WebfingerService;
class SearchController extends Controller class SearchController extends Controller
{ {
public $tokens = []; public $tokens = [];
public $term = ''; public $term = '';
public $hash = ''; public $hash = '';
public $cacheKey = 'api:search:tag:'; public $cacheKey = 'api:search:tag:';
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
} }
public function searchAPI(Request $request) public function searchAPI(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'q' => 'required|string|min:3|max:120', 'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro', 'src' => 'required|string|in:metro',
'v' => 'required|integer|in:2', 'v' => 'required|integer|in:2',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger' 'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
]); ]);
$scope = $request->input('scope') ?? 'all'; $scope = $request->input('scope') ?? 'all';
$this->term = e(urldecode($request->input('q'))); $this->term = e(urldecode($request->input('q')));
$this->hash = hash('sha256', $this->term); $this->hash = hash('sha256', $this->term);
switch ($scope) { switch ($scope) {
case 'all': case 'all':
$this->getHashtags(); $this->getHashtags();
$this->getPosts(); $this->getPosts();
$this->getProfiles(); $this->getProfiles();
// $this->getPlaces(); // $this->getPlaces();
break; break;
case 'hashtag': case 'hashtag':
$this->getHashtags(); $this->getHashtags();
break; break;
case 'profile': case 'profile':
$this->getProfiles(); $this->getProfiles();
break; break;
case 'webfinger': case 'webfinger':
$this->webfingerSearch(); $this->webfingerSearch();
break; break;
case 'remote': case 'remote':
$this->remoteLookupSearch(); $this->remoteLookupSearch();
break; break;
case 'place': case 'place':
$this->getPlaces(); $this->getPlaces();
break; break;
default: default:
break; break;
} }
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT); return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
} }
protected function getPosts() protected function getPosts()
{ {
$tag = $this->term; $tag = $this->term;
$hash = hash('sha256', $tag); $hash = hash('sha256', $tag);
if( Helpers::validateUrl($tag) != false && if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true && Helpers::validateLocalUrl($tag) != true &&
config('federation.activitypub.enabled') == true && config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true config('federation.activitypub.remoteFollow') == true
) { ) {
$remote = Helpers::fetchFromUrl($tag); $remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) && if( isset($remote['type']) &&
$remote['type'] == 'Note') { $remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag); $item = Helpers::statusFetch($tag);
$this->tokens['posts'] = [[ $this->tokens['posts'] = [[
'count' => 0, 'count' => 0,
'url' => $item->url(), 'url' => $item->url(),
'type' => 'status', 'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>", 'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption], 'tokens' => [$item->caption],
'name' => $item->caption, 'name' => $item->caption,
'thumb' => $item->thumb(), 'thumb' => $item->thumb(),
]]; ]];
} }
} else { } else {
$posts = Status::select('id', 'profile_id', 'caption', 'created_at') $posts = Status::select('id', 'profile_id', 'caption', 'created_at')
->whereHas('media') ->whereHas('media')
->whereNull('in_reply_to_id') ->whereNull('in_reply_to_id')
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile_id) ->whereProfileId(Auth::user()->profile_id)
->where('caption', 'like', '%'.$tag.'%') ->where('caption', 'like', '%'.$tag.'%')
->latest() ->latest()
->limit(10) ->limit(10)
->get(); ->get();
if($posts->count() > 0) { if($posts->count() > 0) {
$posts = $posts->map(function($item, $key) { $posts = $posts->map(function($item, $key) {
return [ return [
'count' => 0, 'count' => 0,
'url' => $item->url(), 'url' => $item->url(),
'type' => 'status', 'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>", 'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption], 'tokens' => [$item->caption],
'name' => $item->caption, 'name' => $item->caption,
'thumb' => $item->thumb(), 'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class 'filter' => $item->firstMedia()->filter_class
]; ];
}); });
$this->tokens['posts'] = $posts; $this->tokens['posts'] = $posts;
} }
} }
} }
protected function getHashtags() protected function getHashtags()
{ {
$tag = $this->term; $tag = $this->term;
$key = $this->cacheKey . 'hashtags:' . $this->hash; $key = $this->cacheKey . 'hashtags:' . $this->hash;
$ttl = now()->addMinutes(1); $ttl = now()->addMinutes(1);
$tokens = Cache::remember($key, $ttl, function() use($tag) { $tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag; $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
$hashtags = Hashtag::select('id', 'name', 'slug') $hashtags = Hashtag::select('id', 'name', 'slug')
->where('slug', 'like', '%'.$htag.'%') ->where('slug', 'like', '%'.$htag.'%')
->whereHas('posts') ->whereHas('posts')
->limit(20) ->limit(20)
->get(); ->get();
if($hashtags->count() > 0) { if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) { $tags = $hashtags->map(function ($item, $key) {
return [ return [
'count' => $item->posts()->count(), 'count' => $item->posts()->count(),
'url' => $item->url(), 'url' => $item->url(),
'type' => 'hashtag', 'type' => 'hashtag',
'value' => $item->name, 'value' => $item->name,
'tokens' => '', 'tokens' => '',
'name' => null, 'name' => null,
]; ];
}); });
return $tags; return $tags;
} }
}); });
$this->tokens['hashtags'] = $tokens; $this->tokens['hashtags'] = $tokens;
} }
protected function getPlaces() protected function getPlaces()
{ {
$tag = $this->term; $tag = $this->term;
// $key = $this->cacheKey . 'places:' . $this->hash; // $key = $this->cacheKey . 'places:' . $this->hash;
// $ttl = now()->addHours(12); // $ttl = now()->addHours(12);
// $tokens = Cache::remember($key, $ttl, function() use($tag) { // $tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag]; $htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
$hashtags = Place::select('id', 'name', 'slug', 'country') $hashtags = Place::select('id', 'name', 'slug', 'country')
->where('name', 'like', '%'.$htag[0].'%') ->where('name', 'like', '%'.$htag[0].'%')
->paginate(20); ->paginate(20);
$tags = []; $tags = [];
if($hashtags->count() > 0) { if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) { $tags = $hashtags->map(function ($item, $key) {
return [ return [
'count' => null, 'count' => null,
'url' => $item->url(), 'url' => $item->url(),
'type' => 'place', 'type' => 'place',
'value' => $item->name . ', ' . $item->country, 'value' => $item->name . ', ' . $item->country,
'tokens' => '', 'tokens' => '',
'name' => null, 'name' => null,
'city' => $item->name, 'city' => $item->name,
'country' => $item->country 'country' => $item->country
]; ];
}); });
// return $tags; // return $tags;
} }
// }); // });
$this->tokens['places'] = $tags; $this->tokens['places'] = $tags;
$this->tokens['placesPagination'] = [ $this->tokens['placesPagination'] = [
'total' => $hashtags->total(), 'total' => $hashtags->total(),
'current_page' => $hashtags->currentPage(), 'current_page' => $hashtags->currentPage(),
'last_page' => $hashtags->lastPage() 'last_page' => $hashtags->lastPage()
]; ];
} }
protected function getProfiles() protected function getProfiles()
{ {
$tag = $this->term; $tag = $this->term;
$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash; $remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
$key = $this->cacheKey . 'profiles:' . $this->hash; $key = $this->cacheKey . 'profiles:' . $this->hash;
$remoteTtl = now()->addMinutes(15); $remoteTtl = now()->addMinutes(15);
$ttl = now()->addHours(2); $ttl = now()->addHours(2);
if( Helpers::validateUrl($tag) != false && if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true && Helpers::validateLocalUrl($tag) != true &&
config('federation.activitypub.enabled') == true && config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true config('federation.activitypub.remoteFollow') == true
) { ) {
$remote = Helpers::fetchFromUrl($tag); $remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) && if( isset($remote['type']) &&
$remote['type'] == 'Person' $remote['type'] == 'Person'
) { ) {
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) { $this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
$item = Helpers::profileFirstOrNew($tag); $item = Helpers::profileFirstOrNew($tag);
$tokens = [[ $tokens = [[
'count' => 1, 'count' => 1,
'url' => $item->url(), 'url' => $item->url(),
'type' => 'profile', 'type' => 'profile',
'value' => $item->username, 'value' => $item->username,
'tokens' => [$item->username], 'tokens' => [$item->username],
'name' => $item->name, 'name' => $item->name,
'entity' => [ 'entity' => [
'id' => (string) $item->id, 'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile), 'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id), 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(), 'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain, 'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count() 'post_count' => $item->statuses()->count()
] ]
]]; ]];
return $tokens; return $tokens;
}); });
} }
} }
else { else {
$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) { $this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
if(Str::startsWith($tag, '@')) { if(Str::startsWith($tag, '@')) {
$tag = substr($tag, 1); $tag = substr($tag, 1);
} }
$users = Profile::select('status', 'domain', 'username', 'name', 'id') $users = Profile::select('status', 'domain', 'username', 'name', 'id')
->whereNull('status') ->whereNull('status')
->where('username', 'like', '%'.$tag.'%') ->where('username', 'like', '%'.$tag.'%')
->limit(20) ->limit(20)
->orderBy('domain') ->orderBy('domain')
->get(); ->get();
if($users->count() > 0) { if($users->count() > 0) {
return $users->map(function ($item, $key) { return $users->map(function ($item, $key) {
return [ return [
'count' => 0, 'count' => 0,
'url' => $item->url(), 'url' => $item->url(),
'type' => 'profile', 'type' => 'profile',
'value' => $item->username, 'value' => $item->username,
'tokens' => [$item->username], 'tokens' => [$item->username],
'name' => $item->name, 'name' => $item->name,
'avatar' => $item->avatarUrl(), 'avatar' => $item->avatarUrl(),
'id' => (string) $item->id, 'id' => (string) $item->id,
'entity' => [ 'entity' => [
'id' => (string) $item->id, 'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile), 'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id), 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(), 'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain, 'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count() 'post_count' => $item->statuses()->count()
] ]
]; ];
}); });
} }
}); });
} }
} }
public function results(Request $request) public function results(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'q' => 'required|string|min:1', 'q' => 'required|string|min:1',
]); ]);
return view('search.results');
}
protected function webfingerSearch() return view('search.results');
{ }
$wfs = WebfingerService::lookup($this->term);
if(empty($wfs)) { protected function webfingerSearch()
return; {
} $wfs = WebfingerService::lookup($this->term);
$this->tokens['profiles'] = [ if(empty($wfs)) {
[ return;
'count' => 1, }
'url' => $wfs['url'],
'type' => 'profile',
'value' => $wfs['username'],
'tokens' => [$wfs['username']],
'name' => $wfs['display_name'],
'entity' => [
'id' => (string) $wfs['id'],
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local']
]
]
];
return;
}
protected function remotePostLookup() $this->tokens['profiles'] = [
{ [
$tag = $this->term; 'count' => 1,
$hash = hash('sha256', $tag); 'url' => $wfs['url'],
$local = Helpers::validateLocalUrl($tag); 'type' => 'profile',
$valid = Helpers::validateUrl($tag); 'value' => $wfs['username'],
'tokens' => [$wfs['username']],
'name' => $wfs['display_name'],
'entity' => [
'id' => (string) $wfs['id'],
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local']
]
]
];
return;
}
if($valid == false || $local == true) { protected function remotePostLookup()
return; {
} $tag = $this->term;
$hash = hash('sha256', $tag);
if(Status::whereUri($tag)->whereLocal(false)->exists()) { $local = Helpers::validateLocalUrl($tag);
$item = Status::whereUri($tag)->first(); $valid = Helpers::validateUrl($tag);
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $item->firstMedia()->remote_url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
$remote = Helpers::fetchFromUrl($tag); if($valid == false || $local == true) {
return;
}
if(isset($remote['type']) && $remote['type'] == 'Note') { if(Status::whereUri($tag)->whereLocal(false)->exists()) {
$item = Helpers::statusFetch($tag); $item = Status::whereUri($tag)->first();
$this->tokens['posts'] = [[ $this->tokens['posts'] = [[
'count' => 0, 'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id", 'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status', 'type' => 'status',
'username' => $item->profile->username, 'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption, 'caption' => $item->rendered ?? $item->caption,
'thumb' => $item->firstMedia()->remote_url, 'thumb' => $item->firstMedia()->remote_url,
'timestamp' => $item->created_at->diffForHumans() 'timestamp' => $item->created_at->diffForHumans()
]]; ]];
} }
}
protected function remoteLookupSearch() $remote = Helpers::fetchFromUrl($tag);
{
if(!Helpers::validateUrl($this->term)) { if(isset($remote['type']) && $remote['type'] == 'Note') {
return; $item = Helpers::statusFetch($tag);
} $this->tokens['posts'] = [[
$this->getProfiles(); 'count' => 0,
$this->remotePostLookup(); 'url' => "/i/web/post/_/$item->profile_id/$item->id",
} 'type' => 'status',
} 'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $item->firstMedia()->remote_url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
}
protected function remoteLookupSearch()
{
if(!Helpers::validateUrl($this->term)) {
return;
}
$this->getProfiles();
$this->remotePostLookup();
}
}

View file

@ -16,6 +16,7 @@ use Mail;
use Purify; use Purify;
use App\Mail\PasswordChange; use App\Mail\PasswordChange;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Services\PronounService;
trait HomeSettings trait HomeSettings
{ {
@ -25,23 +26,25 @@ trait HomeSettings
$id = Auth::user()->profile->id; $id = Auth::user()->profile->id;
$storage = []; $storage = [];
$used = Media::whereProfileId($id)->sum('size'); $used = Media::whereProfileId($id)->sum('size');
$storage['limit'] = config('pixelfed.max_account_size') * 1024; $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
$storage['used'] = $used; $storage['used'] = $used;
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100); $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
$storage['limitPretty'] = PrettyNumber::size($storage['limit']); $storage['limitPretty'] = PrettyNumber::size($storage['limit']);
$storage['usedPretty'] = PrettyNumber::size($storage['used']); $storage['usedPretty'] = PrettyNumber::size($storage['used']);
$pronouns = PronounService::get($id);
return view('settings.home', compact('storage')); return view('settings.home', compact('storage', 'pronouns'));
} }
public function homeUpdate(Request $request) public function homeUpdate(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:'.config('pixelfed.max_name_length'), 'name' => 'required|string|max:'.config('pixelfed.max_name_length'),
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'), 'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
'website' => 'nullable|url', 'website' => 'nullable|url',
'language' => 'nullable|string|min:2|max:5' 'language' => 'nullable|string|min:2|max:5',
]); 'pronouns' => 'nullable|array|max:4'
]);
$changes = false; $changes = false;
$name = strip_tags(Purify::clean($request->input('name'))); $name = strip_tags(Purify::clean($request->input('name')));
@ -50,12 +53,14 @@ trait HomeSettings
$language = $request->input('language'); $language = $request->input('language');
$user = Auth::user(); $user = Auth::user();
$profile = $user->profile; $profile = $user->profile;
$pronouns = $request->input('pronouns');
$existingPronouns = PronounService::get($profile->id);
$layout = $request->input('profile_layout'); $layout = $request->input('profile_layout');
if($layout) { if($layout) {
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout; $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
} }
$enforceEmailVerification = config('pixelfed.enforce_email_verification'); $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
// Only allow email to be updated if not yet verified // Only allow email to be updated if not yet verified
if (!$enforceEmailVerification || !$changes && $user->email_verified_at) { if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
@ -82,6 +87,14 @@ trait HomeSettings
$user->language = $language; $user->language = $language;
session()->put('locale', $language); session()->put('locale', $language);
} }
if($existingPronouns != $pronouns) {
if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
PronounService::clear($profile->id);
} else {
PronounService::put($profile->id, $pronouns);
}
}
} }
if ($changes === true) { if ($changes === true) {
@ -152,7 +165,7 @@ trait HomeSettings
$user = Auth::user(); $user = Auth::user();
$profile = $user->profile; $profile = $user->profile;
$validate = config('pixelfed.enforce_email_verification'); $validate = config_cache('pixelfed.enforce_email_verification');
if ($user->email != $email) { if ($user->email != $email) {
$changes = true; $changes = true;
@ -193,4 +206,4 @@ trait HomeSettings
return view('settings.avatar'); return view('settings.avatar');
} }
} }

View file

@ -77,13 +77,13 @@ class SettingsController extends Controller
public function dataImport() public function dataImport()
{ {
abort_if(!config('pixelfed.import.instagram.enabled'), 404); abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.home'); return view('settings.import.home');
} }
public function dataImportInstagram() public function dataImportInstagram()
{ {
abort_if(!config('pixelfed.import.instagram.enabled'), 404); abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
return view('settings.import.instagram.home'); return view('settings.import.instagram.home');
} }

View file

@ -70,11 +70,16 @@ class StatusController extends Controller
]); ]);
} }
if ($request->wantsJson() && config('federation.activitypub.enabled')) { if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $status); return $this->showActivityPub($request, $status);
} }
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show'; $template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
// $template = $status->type === 'video' &&
// $request->has('video_beta') &&
// $request->video_beta == 1 &&
// $request->user() ?
// 'status.show_video' : 'status.show';
return view($template, compact('user', 'status')); return view($template, compact('user', 'status'));
} }
@ -340,7 +345,7 @@ class StatusController extends Controller
public static function mimeTypeCheck($mimes) public static function mimeTypeCheck($mimes)
{ {
$allowed = explode(',', config('pixelfed.media_types')); $allowed = explode(',', config_cache('pixelfed.media_types'));
$count = count($mimes); $count = count($mimes);
$photos = 0; $photos = 0;
$videos = 0; $videos = 0;

View file

@ -21,14 +21,14 @@ class StoryController extends Controller
{ {
public function apiV1Add(Request $request) public function apiV1Add(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'file' => function() { 'file' => function() {
return [ return [
'required', 'required',
'mimes:image/jpeg,image/png,video/mp4', 'mimes:image/jpeg,image/png,video/mp4',
'max:' . config('pixelfed.max_photo_size'), 'max:' . config_cache('pixelfed.max_photo_size'),
]; ];
}, },
]); ]);
@ -78,7 +78,7 @@ class StoryController extends Controller
protected function storePhoto($photo, $user) protected function storePhoto($photo, $user)
{ {
$mimes = explode(',', config('pixelfed.media_types')); $mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [ if(in_array($photo->getMimeType(), [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@ -94,7 +94,7 @@ class StoryController extends Controller
$fpath = storage_path('app/' . $path); $fpath = storage_path('app/' . $path);
$img = Intervention::make($fpath); $img = Intervention::make($fpath);
$img->orientate(); $img->orientate();
$img->save($fpath, config('pixelfed.image_quality')); $img->save($fpath, config_cache('pixelfed.image_quality'));
$img->destroy(); $img->destroy();
} }
return $path; return $path;
@ -102,7 +102,7 @@ class StoryController extends Controller
public function cropPhoto(Request $request) public function cropPhoto(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'media_id' => 'required|integer|min:1', 'media_id' => 'required|integer|min:1',
@ -133,7 +133,7 @@ class StoryController extends Controller
$img->resize(1080, 1920, function ($constraint) { $img->resize(1080, 1920, function ($constraint) {
$constraint->aspectRatio(); $constraint->aspectRatio();
}); });
$img->save($path, config('pixelfed.image_quality')); $img->save($path, config_cache('pixelfed.image_quality'));
} }
return [ return [
@ -144,7 +144,7 @@ class StoryController extends Controller
public function publishStory(Request $request) public function publishStory(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'media_id' => 'required', 'media_id' => 'required',
@ -169,7 +169,7 @@ class StoryController extends Controller
public function apiV1Delete(Request $request, $id) public function apiV1Delete(Request $request, $id)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user(); $user = $request->user();
@ -190,7 +190,7 @@ class StoryController extends Controller
public function apiV1Recent(Request $request) public function apiV1Recent(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = $request->user()->profile; $profile = $request->user()->profile;
$following = $profile->following->pluck('id')->toArray(); $following = $profile->following->pluck('id')->toArray();
@ -232,7 +232,7 @@ class StoryController extends Controller
public function apiV1Fetch(Request $request, $id) public function apiV1Fetch(Request $request, $id)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile; $authed = $request->user()->profile;
$profile = Profile::findOrFail($id); $profile = Profile::findOrFail($id);
@ -270,7 +270,7 @@ class StoryController extends Controller
public function apiV1Item(Request $request, $id) public function apiV1Item(Request $request, $id)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile; $authed = $request->user()->profile;
$story = Story::with('profile') $story = Story::with('profile')
@ -304,7 +304,7 @@ class StoryController extends Controller
public function apiV1Profile(Request $request, $id) public function apiV1Profile(Request $request, $id)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$authed = $request->user()->profile; $authed = $request->user()->profile;
$profile = Profile::findOrFail($id); $profile = Profile::findOrFail($id);
@ -355,7 +355,7 @@ class StoryController extends Controller
public function apiV1Viewed(Request $request) public function apiV1Viewed(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'id' => 'required|integer|min:1|exists:stories', 'id' => 'required|integer|min:1|exists:stories',
@ -391,7 +391,7 @@ class StoryController extends Controller
public function apiV1Exists(Request $request, $id) public function apiV1Exists(Request $request, $id)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$res = (bool) Story::whereProfileId($id) $res = (bool) Story::whereProfileId($id)
->whereActive(true) ->whereActive(true)
@ -403,7 +403,7 @@ class StoryController extends Controller
public function apiV1Me(Request $request) public function apiV1Me(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = $request->user()->profile; $profile = $request->user()->profile;
$stories = Story::whereProfileId($profile->id) $stories = Story::whereProfileId($profile->id)
@ -441,14 +441,14 @@ class StoryController extends Controller
public function compose(Request $request) public function compose(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
return view('stories.compose'); return view('stories.compose');
} }
public function iRedirect(Request $request) public function iRedirect(Request $request)
{ {
abort_if(!config('instance.stories.enabled') || !$request->user(), 404); abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user(); $user = $request->user();
abort_if(!$user, 404); abort_if(!$user, 404);

View file

@ -6,31 +6,31 @@ use Closure;
class EmailVerificationCheck class EmailVerificationCheck
{ {
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* *
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
if ($request->user() && if ($request->user() &&
config('pixelfed.enforce_email_verification') && config_cache('pixelfed.enforce_email_verification') &&
is_null($request->user()->email_verified_at) && is_null($request->user()->email_verified_at) &&
!$request->is( !$request->is(
'i/auth/*', 'i/auth/*',
'i/verify-email', 'i/verify-email',
'log*', 'log*',
'i/confirm-email/*', 'i/confirm-email/*',
'settings/home', 'settings/home',
'settings/email' 'settings/email'
) )
) { ) {
return redirect('/i/verify-email'); return redirect('/i/verify-email');
} }
return $next($request); return $next($request);
} }
} }

View file

@ -16,67 +16,67 @@ use Image as Intervention;
class AvatarOptimize implements ShouldQueue class AvatarOptimize implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile; protected $profile;
protected $current; protected $current;
/** /**
* Delete the job if its models no longer exist. * Delete the job if its models no longer exist.
* *
* @var bool * @var bool
*/ */
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Profile $profile, $current) public function __construct(Profile $profile, $current)
{ {
$this->profile = $profile; $this->profile = $profile;
$this->current = $current; $this->current = $current;
} }
/** /**
* Execute the job. * Execute the job.
* *
* @return void * @return void
*/ */
public function handle() public function handle()
{ {
$avatar = $this->profile->avatar; $avatar = $this->profile->avatar;
$file = storage_path("app/$avatar->media_path"); $file = storage_path("app/$avatar->media_path");
try { try {
$img = Intervention::make($file)->orientate(); $img = Intervention::make($file)->orientate();
$img->fit(200, 200, function ($constraint) { $img->fit(200, 200, function ($constraint) {
$constraint->upsize(); $constraint->upsize();
}); });
$quality = config('pixelfed.image_quality'); $quality = config_cache('pixelfed.image_quality');
$img->save($file, $quality); $img->save($file, $quality);
$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail(); $avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
$avatar->change_count = ++$avatar->change_count; $avatar->change_count = ++$avatar->change_count;
$avatar->last_processed_at = Carbon::now(); $avatar->last_processed_at = Carbon::now();
$avatar->save(); $avatar->save();
Cache::forget('avatar:' . $avatar->profile_id); Cache::forget('avatar:' . $avatar->profile_id);
$this->deleteOldAvatar($avatar->media_path, $this->current); $this->deleteOldAvatar($avatar->media_path, $this->current);
} catch (Exception $e) { } catch (Exception $e) {
} }
} }
protected function deleteOldAvatar($new, $current) protected function deleteOldAvatar($new, $current)
{ {
if ( storage_path('app/'.$new) == $current || if ( storage_path('app/'.$new) == $current ||
Str::endsWith($current, 'avatars/default.png') || Str::endsWith($current, 'avatars/default.png') ||
Str::endsWith($current, 'avatars/default.jpg')) Str::endsWith($current, 'avatars/default.jpg'))
{ {
return; return;
} }
if (is_file($current)) { if (is_file($current)) {
@unlink($current); @unlink($current);
} }
} }
} }

View file

@ -49,7 +49,7 @@ class ImportInstagram implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
if(config('pixelfed.import.instagram.enabled') != true) { if(config_cache('pixelfed.import.instagram.enabled') != true) {
return; return;
} }

View file

@ -18,132 +18,132 @@ use App\Util\ActivityPub\HttpSignature;
class SharePipeline implements ShouldQueue class SharePipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
/** /**
* Delete the job if its models no longer exist. * Delete the job if its models no longer exist.
* *
* @var bool * @var bool
*/ */
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Status $status) public function __construct(Status $status)
{ {
$this->status = $status; $this->status = $status;
} }
/** /**
* Execute the job. * Execute the job.
* *
* @return void * @return void
*/ */
public function handle() public function handle()
{ {
$status = $this->status; $status = $this->status;
$actor = $status->profile; $actor = $status->profile;
$target = $status->parent()->profile; $target = $status->parent()->profile;
if ($status->uri !== null) { if ($status->uri !== null) {
// Ignore notifications to remote statuses // Ignore notifications to remote statuses
return; return;
} }
$exists = Notification::whereProfileId($target->id) $exists = Notification::whereProfileId($target->id)
->whereActorId($status->profile_id) ->whereActorId($status->profile_id)
->whereAction('share') ->whereAction('share')
->whereItemId($status->reblog_of_id) ->whereItemId($status->reblog_of_id)
->whereItemType('App\Status') ->whereItemType('App\Status')
->count(); ->count();
if ($target->id === $status->profile_id) { if ($target->id === $status->profile_id) {
$this->remoteAnnounceDeliver(); $this->remoteAnnounceDeliver();
return true; return true;
} }
if( $exists !== 0) { if( $exists !== 0) {
return true; return true;
} }
$this->remoteAnnounceDeliver(); $this->remoteAnnounceDeliver();
try { try {
$notification = new Notification; $notification = new Notification;
$notification->profile_id = $target->id; $notification->profile_id = $target->id;
$notification->actor_id = $actor->id; $notification->actor_id = $actor->id;
$notification->action = 'share'; $notification->action = 'share';
$notification->message = $status->shareToText(); $notification->message = $status->shareToText();
$notification->rendered = $status->shareToHtml(); $notification->rendered = $status->shareToHtml();
$notification->item_id = $status->reblog_of_id ?? $status->id; $notification->item_id = $status->reblog_of_id ?? $status->id;
$notification->item_type = "App\Status"; $notification->item_type = "App\Status";
$notification->save(); $notification->save();
$redis = Redis::connection(); $redis = Redis::connection();
$key = config('cache.prefix').':user.'.$status->profile_id.'.notifications'; $key = config('cache.prefix').':user.'.$status->profile_id.'.notifications';
$redis->lpush($key, $notification->id); $redis->lpush($key, $notification->id);
} catch (Exception $e) { } catch (Exception $e) {
Log::error($e); Log::error($e);
} }
} }
public function remoteAnnounceDeliver() public function remoteAnnounceDeliver()
{ {
if(config('federation.activitypub.enabled') == false) { if(config_cache('federation.activitypub.enabled') == false) {
return true; return true;
} }
$status = $this->status; $status = $this->status;
$profile = $status->profile; $profile = $status->profile;
$fractal = new Fractal\Manager(); $fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer()); $fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new Announce()); $resource = new Fractal\Resource\Item($status, new Announce());
$activity = $fractal->createData($resource)->toArray(); $activity = $fractal->createData($resource)->toArray();
$audience = $status->profile->getAudienceInbox(); $audience = $status->profile->getAudienceInbox();
if(empty($audience) || $status->scope != 'public') { if(empty($audience) || $status->scope != 'public') {
// Return on profiles with no remote followers // Return on profiles with no remote followers
return; return;
} }
$payload = json_encode($activity); $payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$requests = function($audience) use ($client, $activity, $profile, $payload) { $client = new Client([
foreach($audience as $url) { 'timeout' => config('federation.activitypub.delivery.timeout')
$headers = HttpSignature::sign($profile, $url, $activity); ]);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [ $requests = function($audience) use ($client, $activity, $profile, $payload) {
'concurrency' => config('federation.activitypub.delivery.concurrency'), foreach($audience as $url) {
'fulfilled' => function ($response, $index) { $headers = HttpSignature::sign($profile, $url, $activity);
}, yield function() use ($client, $url, $headers, $payload) {
'rejected' => function ($reason, $index) { return $client->postAsync($url, [
} 'curl' => [
]); CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
$promise = $pool->promise(); CURLOPT_HEADER => true
]
]);
};
}
};
$promise->wait(); $pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
} $promise = $pool->promise();
$promise->wait();
}
} }

View file

@ -4,13 +4,14 @@ namespace App\Jobs\StatusPipeline;
use DB, Storage; use DB, Storage;
use App\{ use App\{
AccountInterstitial, AccountInterstitial,
MediaTag, MediaTag,
Notification, Notification,
Report, Report,
Status, Status,
StatusHashtag, StatusHashtag,
}; };
use App\Models\StatusVideo;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -30,150 +31,149 @@ use App\Services\MediaStorageService;
class StatusDelete implements ShouldQueue class StatusDelete implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
/** /**
* Execute the job. * Delete the job if its models no longer exist.
* *
* @return void * @var bool
*/ */
public function handle() public $deleteWhenMissingModels = true;
{
$status = $this->status;
$profile = $this->status->profile;
StatusService::del($status->id); /**
$count = $profile->statuses() * Create a new job instance.
->getQuery() *
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) * @return void
->whereNull('in_reply_to_id') */
->whereNull('reblog_of_id') public function __construct(Status $status)
->count(); {
$this->status = $status;
}
$profile->status_count = ($count - 1); /**
$profile->save(); * Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
$profile = $this->status->profile;
if(config('federation.activitypub.enabled') == true) { StatusService::del($status->id);
$this->fanoutDelete($status); $count = $profile->statuses()
} else { ->getQuery()
$this->unlinkRemoveMedia($status); ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
} ->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->count();
} $profile->status_count = ($count - 1);
$profile->save();
public function unlinkRemoveMedia($status) if(config_cache('federation.activitypub.enabled') == true) {
{ $this->fanoutDelete($status);
foreach ($status->media as $media) { } else {
MediaStorageService::delete($media, true); $this->unlinkRemoveMedia($status);
} }
if($status->in_reply_to_id) { }
DB::transaction(function() use($status) {
$parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count;
$parent->save();
});
}
DB::transaction(function() use($status) {
$comments = Status::where('in_reply_to_id', $status->id)->get();
foreach ($comments as $comment) {
$comment->in_reply_to_id = null;
$comment->save();
Notification::whereItemType('App\Status')
->whereItemId($comment->id)
->delete();
}
$status->likes()->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->delete();
StatusHashtag::whereStatusId($status->id)->delete();
Report::whereObjectType('App\Status')
->whereObjectId($status->id)
->delete();
MediaTag::where('status_id', $status->id) public function unlinkRemoveMedia($status)
->cursor() {
->each(function($tag) { foreach ($status->media as $media) {
Notification::where('item_type', 'App\MediaTag') MediaStorageService::delete($media, true);
->where('item_id', $tag->id) }
->forceDelete();
$tag->delete();
});
AccountInterstitial::where('item_type', 'App\Status') if($status->in_reply_to_id) {
->where('item_id', $status->id) DB::transaction(function() use($status) {
->delete(); $parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count;
$parent->save();
});
}
DB::transaction(function() use($status) {
$comments = Status::where('in_reply_to_id', $status->id)->get();
foreach ($comments as $comment) {
$comment->in_reply_to_id = null;
$comment->save();
Notification::whereItemType('App\Status')
->whereItemId($comment->id)
->delete();
}
$status->likes()->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->delete();
StatusHashtag::whereStatusId($status->id)->delete();
Report::whereObjectType('App\Status')
->whereObjectId($status->id)
->delete();
MediaTag::where('status_id', $status->id)
->cursor()
->each(function($tag) {
Notification::where('item_type', 'App\MediaTag')
->where('item_id', $tag->id)
->forceDelete();
$tag->delete();
});
StatusVideo::whereStatusId($status->id)->delete();
AccountInterstitial::where('item_type', 'App\Status')
->where('item_id', $status->id)
->delete();
$status->forceDelete(); $status->forceDelete();
}); });
return true; return true;
} }
protected function fanoutDelete($status) protected function fanoutDelete($status)
{ {
$audience = $status->profile->getAudienceInbox(); $audience = $status->profile->getAudienceInbox();
$profile = $status->profile; $profile = $status->profile;
$fractal = new Fractal\Manager(); $fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer()); $fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new DeleteNote()); $resource = new Fractal\Resource\Item($status, new DeleteNote());
$activity = $fractal->createData($resource)->toArray(); $activity = $fractal->createData($resource)->toArray();
$this->unlinkRemoveMedia($status); $this->unlinkRemoveMedia($status);
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$requests = function($audience) use ($client, $activity, $profile, $payload) { $payload = json_encode($activity);
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [ $client = new Client([
'concurrency' => config('federation.activitypub.delivery.concurrency'), 'timeout' => config('federation.activitypub.delivery.timeout')
'fulfilled' => function ($response, $index) { ]);
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait(); $requests = function($audience) use ($client, $activity, $profile, $payload) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
} $pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
}
} }

View file

@ -21,146 +21,146 @@ use Illuminate\Queue\SerializesModels;
class StatusEntityLexer implements ShouldQueue class StatusEntityLexer implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
protected $entities; protected $entities;
protected $autolink; protected $autolink;
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status;
}
/** /**
* Execute the job. * Delete the job if its models no longer exist.
* *
* @return void * @var bool
*/ */
public function handle() public $deleteWhenMissingModels = true;
{
$profile = $this->status->profile;
$count = $profile->statuses() /**
->getQuery() * Create a new job instance.
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) *
->whereNull('in_reply_to_id') * @return void
->whereNull('reblog_of_id') */
->count(); public function __construct(Status $status)
{
$this->status = $status;
}
$profile->status_count = $count; /**
$profile->save(); * Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->status->profile;
if($profile->no_autolink == false) { $count = $profile->statuses()
$this->parseEntities(); ->getQuery()
} ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
} ->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->count();
public function parseEntities() $profile->status_count = $count;
{ $profile->save();
$this->extractEntities();
}
public function extractEntities() if($profile->no_autolink == false) {
{ $this->parseEntities();
$this->entities = Extractor::create()->extract($this->status->caption); }
$this->autolinkStatus(); }
}
public function autolinkStatus() public function parseEntities()
{ {
$this->autolink = Autolink::create()->autolink($this->status->caption); $this->extractEntities();
$this->storeEntities(); }
}
public function storeEntities() public function extractEntities()
{ {
$this->storeHashtags(); $this->entities = Extractor::create()->extract($this->status->caption);
DB::transaction(function () { $this->autolinkStatus();
$status = $this->status; }
$status->rendered = nl2br($this->autolink);
$status->entities = json_encode($this->entities);
$status->save();
});
}
public function storeHashtags() public function autolinkStatus()
{ {
$tags = array_unique($this->entities['hashtags']); $this->autolink = Autolink::create()->autolink($this->status->caption);
$status = $this->status; $this->storeEntities();
}
foreach ($tags as $tag) { public function storeEntities()
if(mb_strlen($tag) > 124) { {
continue; $this->storeHashtags();
} DB::transaction(function () {
DB::transaction(function () use ($status, $tag) { $status = $this->status;
$slug = str_slug($tag, '-', false); $status->rendered = nl2br($this->autolink);
$hashtag = Hashtag::firstOrCreate( $status->entities = json_encode($this->entities);
['name' => $tag, 'slug' => $slug] $status->save();
); });
StatusHashtag::firstOrCreate( }
[
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
'status_visibility' => $status->visibility,
]
);
});
}
$this->storeMentions();
}
public function storeMentions() public function storeHashtags()
{ {
$mentions = array_unique($this->entities['mentions']); $tags = array_unique($this->entities['hashtags']);
$status = $this->status; $status = $this->status;
foreach ($mentions as $mention) { foreach ($tags as $tag) {
$mentioned = Profile::whereUsername($mention)->first(); if(mb_strlen($tag) > 124) {
continue;
}
DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag, '-', false);
$hashtag = Hashtag::firstOrCreate(
['name' => $tag, 'slug' => $slug]
);
StatusHashtag::firstOrCreate(
[
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
'status_visibility' => $status->visibility,
]
);
});
}
$this->storeMentions();
}
if (empty($mentioned) || !isset($mentioned->id)) { public function storeMentions()
continue; {
} $mentions = array_unique($this->entities['mentions']);
$status = $this->status;
DB::transaction(function () use ($status, $mentioned) { foreach ($mentions as $mention) {
$m = new Mention(); $mentioned = Profile::whereUsername($mention)->first();
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
MentionPipeline::dispatch($status, $m); if (empty($mentioned) || !isset($mentioned->id)) {
}); continue;
} }
$this->deliver();
}
public function deliver() DB::transaction(function () use ($status, $mentioned) {
{ $m = new Mention();
$status = $this->status; $m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
if(config('pixelfed.bouncer.enabled')) { MentionPipeline::dispatch($status, $m);
Bouncer::get($status); });
} }
$this->deliver();
}
if($status->uri == null && $status->scope == 'public') { public function deliver()
PublicTimelineService::add($status->id); {
} $status = $this->status;
if(config('federation.activitypub.enabled') == true && config('app.env') == 'production') { if(config_cache('pixelfed.bouncer.enabled')) {
StatusActivityPubDeliver::dispatch($this->status); Bouncer::get($status);
} }
}
if($status->uri == null && $status->scope == 'public') {
PublicTimelineService::add($status->id);
}
if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
StatusActivityPubDeliver::dispatch($this->status);
}
}
} }

View file

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ConfigCache extends Model
{
use HasFactory;
protected $table = 'config_cache';
public $fillable = ['*'];
}

View file

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserPronoun extends Model
{
use HasFactory;
}

View file

@ -26,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
{ {
$this->registerPolicies(); $this->registerPolicies();
if(config('pixelfed.oauth_enabled')) { if(config_cache('pixelfed.oauth_enabled')) {
Passport::routes(null, ['middleware' => ['twofactor', \Fruitcake\Cors\HandleCors::class]]); Passport::routes(null, ['middleware' => ['twofactor', \Fruitcake\Cors\HandleCors::class]]);
Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 15))); Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 15)));
Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 30))); Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 30)));

View file

@ -0,0 +1,95 @@
<?php
namespace App\Services;
use Cache;
use Config;
use App\Models\ConfigCache as ConfigCacheModel;
class ConfigCacheService
{
const CACHE_KEY = 'config_cache:_key:';
public static function get($key)
{
$cacheKey = "config_cache:_key:{$key}";
$ttl = now()->addHours(12);
return Cache::remember($cacheKey, $ttl, function() use($key) {
$allowed = [
'app.name',
'app.short_description',
'app.description',
'app.rules',
'pixelfed.max_photo_size',
'pixelfed.max_album_length',
'pixelfed.image_quality',
'pixelfed.media_types',
'pixelfed.open_registration',
'federation.activitypub.enabled',
'pixelfed.oauth_enabled',
'instance.stories.enabled',
'pixelfed.import.instagram.enabled',
'pixelfed.bouncer.enabled',
'pixelfed.enforce_email_verification',
'pixelfed.max_account_size',
'pixelfed.enforce_account_limit',
'uikit.custom.css',
'uikit.custom.js',
'uikit.show_custom.css',
'uikit.show_custom.js'
];
if(!config('instance.enable_cc')) {
return config($key);
}
if(!in_array($key, $allowed)) {
return config($key);
}
$v = config($key);
$c = ConfigCacheModel::where('k', $key)->first();
if($c) {
return $c->v ?? config($key);
}
if(!$v) {
return;
}
$cc = new ConfigCacheModel;
$cc->k = $key;
$cc->v = $v;
$cc->save();
return $v;
});
}
public static function put($key, $val)
{
$exists = ConfigCacheModel::whereK($key)->first();
if($exists) {
$exists->v = $val;
$exists->save();
Cache::forget(self::CACHE_KEY . $key);
return self::get($key);
}
$cc = new ConfigCacheModel;
$cc->k = $key;
$cc->v = $val;
$cc->save();
Cache::forget(self::CACHE_KEY . $key);
return self::get($key);
}
}

View file

@ -43,11 +43,11 @@ class MediaStorageService {
$h = $r->getHeaders(); $h = $r->getHeaders();
if (isset($h['Content-Length'], $h['Content-Type']) == false || if (isset($h['Content-Length'], $h['Content-Type']) == false ||
empty($h['Content-Length']) || empty($h['Content-Length']) ||
empty($h['Content-Type']) || empty($h['Content-Type']) ||
$h['Content-Length'] < 10 || $h['Content-Length'] < 10 ||
$h['Content-Length'] > (config('pixelfed.max_photo_size') * 1000) $h['Content-Length'] > (config_cache('pixelfed.max_photo_size') * 1000)
) { ) {
return false; return false;
} }
@ -77,7 +77,7 @@ class MediaStorageService {
$pt = explode('/', $media->thumbnail_path); $pt = explode('/', $media->thumbnail_path);
$thumbname = array_pop($pt); $thumbname = array_pop($pt);
$storagePath = implode('/', $p); $storagePath = implode('/', $p);
$disk = Storage::disk(config('filesystems.cloud')); $disk = Storage::disk(config('filesystems.cloud'));
$file = $disk->putFileAs($storagePath, new File($path), $name, 'public'); $file = $disk->putFileAs($storagePath, new File($path), $name, 'public');
$url = $disk->url($file); $url = $disk->url($file);
@ -102,11 +102,11 @@ class MediaStorageService {
} }
$head = $this->head($media->remote_url); $head = $this->head($media->remote_url);
if(!$head) { if(!$head) {
return; return;
} }
$mimes = [ $mimes = [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@ -114,7 +114,7 @@ class MediaStorageService {
]; ];
$mime = $head['mime']; $mime = $head['mime'];
$max_size = (int) config('pixelfed.max_photo_size') * 1000; $max_size = (int) config_cache('pixelfed.max_photo_size') * 1000;
$media->size = $head['length']; $media->size = $head['length'];
$media->remote_media = true; $media->remote_media = true;
$media->save(); $media->save();
@ -247,4 +247,4 @@ class MediaStorageService {
} }
MediaDeletePipeline::dispatch($media); MediaDeletePipeline::dispatch($media);
} }
} }

View file

@ -0,0 +1,102 @@
<?php
namespace App\Services;
use Cache;
use App\Models\UserPronoun;
use App\Profile;
class PronounService {
public static function get($id)
{
$key = 'user:pronouns:' . $id;
$ttl = now()->addHours(12);
return Cache::remember($key, $ttl, function() use($id) {
$res = UserPronoun::whereProfileId($id)->first();
return $res ? json_decode($res->pronouns, true) : [];
});
}
public static function put($id, $pronouns)
{
$res = UserPronoun::whereProfileId($id)->first();
$key = 'user:pronouns:' . $id;
if($res) {
$res->pronouns = json_encode($pronouns);
$res->save();
Cache::forget($key);
AccountService::del($id);
return $res->pronouns;
}
$res = new UserPronoun;
$res->profile_id = $id;
$res->pronouns = json_encode($pronouns);
$res->save();
Cache::forget($key);
AccountService::del($id);
return $res->pronouns;
}
public static function clear($id)
{
$res = UserPronoun::whereProfileId($id)->first();
if($res) {
$res->pronouns = null;
$res->save();
}
$key = 'user:pronouns:' . $id;
Cache::forget($key);
AccountService::del($id);
}
public static function pronouns()
{
return [
'co',
'cos',
'e',
'ey',
'em',
'eir',
'fae',
'faer',
'he',
'him',
'his',
'her',
'hers',
'hir',
'mer',
'mers',
'ne',
'nir',
'nirs',
'nee',
'ner',
'ners',
'per',
'pers',
'she',
'they',
'them',
'theirs',
'thon',
'thons',
've',
'ver',
'vis',
'vi',
'vir',
'xe',
'xem',
'xyr',
'ze',
'zir',
'zie'
];
}
}

View file

@ -5,6 +5,7 @@ namespace App\Transformer\Api;
use Auth; use Auth;
use App\Profile; use App\Profile;
use League\Fractal; use League\Fractal;
use App\Services\PronounService;
class AccountTransformer extends Fractal\TransformerAbstract class AccountTransformer extends Fractal\TransformerAbstract
{ {
@ -35,7 +36,8 @@ class AccountTransformer extends Fractal\TransformerAbstract
'is_admin' => (bool) $is_admin, 'is_admin' => (bool) $is_admin,
'created_at' => $profile->created_at->toJSON(), 'created_at' => $profile->created_at->toJSON(),
'header_bg' => $profile->header_bg, 'header_bg' => $profile->header_bg,
'last_fetched_at' => optional($profile->last_fetched_at)->toJSON() 'last_fetched_at' => optional($profile->last_fetched_at)->toJSON(),
'pronouns' => PronounService::get($profile->id)
]; ];
} }

View file

@ -63,7 +63,7 @@ class Helpers {
$activity = $data['object']; $activity = $data['object'];
$mimeTypes = explode(',', config('pixelfed.media_types')); $mimeTypes = explode(',', config_cache('pixelfed.media_types'));
$mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image']; $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video'] : ['Document', 'Image'];
if(!isset($activity['attachment']) || empty($activity['attachment'])) { if(!isset($activity['attachment']) || empty($activity['attachment'])) {
@ -418,7 +418,7 @@ class Helpers {
$attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment']; $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
$user = $status->profile; $user = $status->profile;
$storagePath = MediaPathService::get($user, 2); $storagePath = MediaPathService::get($user, 2);
$allowed = explode(',', config('pixelfed.media_types')); $allowed = explode(',', config_cache('pixelfed.media_types'));
foreach($attachments as $media) { foreach($attachments as $media) {
$type = $media['mediaType']; $type = $media['mediaType'];

View file

@ -115,8 +115,8 @@ class Inbox
{ {
$activity = $this->payload['object']; $activity = $this->payload['object'];
if(isset($activity['inReplyTo']) && if(isset($activity['inReplyTo']) &&
!empty($activity['inReplyTo']) && !empty($activity['inReplyTo']) &&
Helpers::validateUrl($activity['inReplyTo']) Helpers::validateUrl($activity['inReplyTo'])
) { ) {
// reply detected, skip attachment check // reply detected, skip attachment check
@ -147,8 +147,8 @@ class Inbox
} }
$to = $activity['to']; $to = $activity['to'];
$cc = isset($activity['cc']) ? $activity['cc'] : []; $cc = isset($activity['cc']) ? $activity['cc'] : [];
if(count($to) == 1 && if(count($to) == 1 &&
count($cc) == 0 && count($cc) == 0 &&
parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
) { ) {
$this->handleDirectMessage(); $this->handleDirectMessage();
@ -175,7 +175,7 @@ class Inbox
$inReplyTo = $activity['inReplyTo']; $inReplyTo = $activity['inReplyTo'];
$url = isset($activity['url']) ? $activity['url'] : $activity['id']; $url = isset($activity['url']) ? $activity['url'] : $activity['id'];
Helpers::statusFirstOrFetch($url, true); Helpers::statusFirstOrFetch($url, true);
return; return;
} }
@ -251,8 +251,8 @@ class Inbox
if(count($activity['attachment'])) { if(count($activity['attachment'])) {
$photos = 0; $photos = 0;
$videos = 0; $videos = 0;
$allowed = explode(',', config('pixelfed.media_types')); $allowed = explode(',', config_cache('pixelfed.media_types'));
$activity['attachment'] = array_slice($activity['attachment'], 0, config('pixelfed.max_album_length')); $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length'));
foreach($activity['attachment'] as $a) { foreach($activity['attachment'] as $a) {
$type = $a['mediaType']; $type = $a['mediaType'];
$url = $a['url']; $url = $a['url'];
@ -293,7 +293,7 @@ class Inbox
$dm->type = 'link'; $dm->type = 'link';
$dm->meta = [ $dm->meta = [
'domain' => parse_url($msgText, PHP_URL_HOST), 'domain' => parse_url($msgText, PHP_URL_HOST),
'local' => parse_url($msgText, PHP_URL_HOST) == 'local' => parse_url($msgText, PHP_URL_HOST) ==
parse_url(config('app.url'), PHP_URL_HOST) parse_url(config('app.url'), PHP_URL_HOST)
]; ];
$dm->save(); $dm->save();
@ -459,8 +459,8 @@ class Inbox
public function handleDeleteActivity() public function handleDeleteActivity()
{ {
if(!isset( if(!isset(
$this->payload['actor'], $this->payload['actor'],
$this->payload['object'] $this->payload['object']
)) { )) {
return; return;
} }
@ -510,7 +510,7 @@ class Inbox
$status->delete(); $status->delete();
return; return;
break; break;
default: default:
return; return;
break; break;
@ -564,7 +564,7 @@ class Inbox
switch ($obj['type']) { switch ($obj['type']) {
case 'Accept': case 'Accept':
break; break;
case 'Announce': case 'Announce':
$obj = $obj['object']; $obj = $obj['object'];
if(!Helpers::validateLocalUrl($obj)) { if(!Helpers::validateLocalUrl($obj)) {
@ -603,7 +603,7 @@ class Inbox
->whereItemType('App\Profile') ->whereItemType('App\Profile')
->forceDelete(); ->forceDelete();
break; break;
case 'Like': case 'Like':
$status = Helpers::statusFirstOrFetch($obj['object']); $status = Helpers::statusFirstOrFetch($obj['object']);
if(!$status) { if(!$status) {

View file

@ -13,7 +13,7 @@ class Outbox {
public static function get($profile) public static function get($profile)
{ {
abort_if(!config('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404); abort_if(!config('federation.activitypub.outbox'), 404);
if($profile->status != null) { if($profile->status != null) {
@ -48,4 +48,4 @@ class Outbox {
return $outbox; return $outbox;
} }
} }

View file

@ -154,7 +154,7 @@ class Image
} }
} }
$media->metadata = json_encode($meta); $media->metadata = json_encode($meta);
} }
$img->resize($aspect['width'], $aspect['height'], function ($constraint) { $img->resize($aspect['width'], $aspect['height'], function ($constraint) {
$constraint->aspectRatio(); $constraint->aspectRatio();
@ -163,7 +163,7 @@ class Image
$converted = $this->setBaseName($path, $thumbnail, $img->extension); $converted = $this->setBaseName($path, $thumbnail, $img->extension);
$newPath = storage_path('app/'.$converted['path']); $newPath = storage_path('app/'.$converted['path']);
$quality = config('pixelfed.image_quality'); $quality = config_cache('pixelfed.image_quality');
$img->save($newPath, $quality); $img->save($newPath, $quality);
if ($thumbnail == true) { if ($thumbnail == true) {

View file

@ -8,26 +8,26 @@ use Illuminate\Support\Str;
class Config { class Config {
public static function get() { public static function get() {
return Cache::remember('api:site:configuration:_v0.2', now()->addHours(30), function() { return Cache::remember('api:site:configuration:_v0.2', now()->addMinutes(5), function() {
return [ return [
'open_registration' => config('pixelfed.open_registration'), 'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'uploader' => [ 'uploader' => [
'max_photo_size' => config('pixelfed.max_photo_size'), 'max_photo_size' => config('pixelfed.max_photo_size'),
'max_caption_length' => config('pixelfed.max_caption_length'), 'max_caption_length' => config('pixelfed.max_caption_length'),
'album_limit' => config('pixelfed.max_album_length'), 'album_limit' => config_cache('pixelfed.max_album_length'),
'image_quality' => config('pixelfed.image_quality'), 'image_quality' => config_cache('pixelfed.image_quality'),
'max_collection_length' => config('pixelfed.max_collection_length', 18), 'max_collection_length' => config('pixelfed.max_collection_length', 18),
'optimize_image' => config('pixelfed.optimize_image'), 'optimize_image' => config('pixelfed.optimize_image'),
'optimize_video' => config('pixelfed.optimize_video'), 'optimize_video' => config('pixelfed.optimize_video'),
'media_types' => config('pixelfed.media_types'), 'media_types' => config_cache('pixelfed.media_types'),
'enforce_account_limit' => config('pixelfed.enforce_account_limit') 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit')
], ],
'activitypub' => [ 'activitypub' => [
'enabled' => config('federation.activitypub.enabled'), 'enabled' => config_cache('federation.activitypub.enabled'),
'remote_follow' => config('federation.activitypub.remoteFollow') 'remote_follow' => config('federation.activitypub.remoteFollow')
], ],
@ -39,10 +39,10 @@ class Config {
], ],
'site' => [ 'site' => [
'name' => config('app.name', 'pixelfed'), 'name' => config_cache('app.name'),
'domain' => config('pixelfed.domain.app'), 'domain' => config('pixelfed.domain.app'),
'url' => config('app.url'), 'url' => config('app.url'),
'description' => config('instance.description') 'description' => config_cache('app.short_description')
], ],
'username' => [ 'username' => [
@ -54,12 +54,12 @@ class Config {
], ],
'features' => [ 'features' => [
'mobile_apis' => config('pixelfed.oauth_enabled'), 'mobile_apis' => config_cache('pixelfed.oauth_enabled'),
'circles' => false, 'circles' => false,
'stories' => config('instance.stories.enabled'), 'stories' => config_cache('instance.stories.enabled'),
'video' => Str::contains(config('pixelfed.media_types'), 'video/mp4'), 'video' => Str::contains(config_cache('pixelfed.media_types'), 'video/mp4'),
'import' => [ 'import' => [
'instagram' => config('pixelfed.import.instagram.enabled'), 'instagram' => config_cache('pixelfed.import.instagram.enabled'),
'mastodon' => false, 'mastodon' => false,
'pixelfed' => false 'pixelfed' => false
], ],

View file

@ -10,68 +10,68 @@ class Nodeinfo {
public static function get() public static function get()
{ {
$res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () { $res = Cache::remember('api:nodeinfo', now()->addMinutes(15), function () {
$activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() { $activeHalfYear = Cache::remember('api:nodeinfo:ahy', now()->addHours(12), function() {
// todo: replace with last_active_at after July 9, 2021 (96afc3e781) // todo: replace with last_active_at after July 9, 2021 (96afc3e781)
$count = collect([]); $count = collect([]);
$likes = Like::select('profile_id')->with('actor')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray(); $likes = Like::select('profile_id')->with('actor')->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->get()->filter(function($like) {return $like->actor && $like->actor->domain == null;})->pluck('profile_id')->toArray();
$count = $count->merge($likes); $count = $count->merge($likes);
$statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray(); $statuses = Status::select('profile_id')->whereLocal(true)->where('created_at', '>', now()->subMonths(6)->toDateTimeString())->groupBy('profile_id')->pluck('profile_id')->toArray();
$count = $count->merge($statuses); $count = $count->merge($statuses);
$profiles = User::select('profile_id', 'last_active_at') $profiles = User::select('profile_id', 'last_active_at')
->whereNotNull('last_active_at') ->whereNotNull('last_active_at')
->where('last_active_at', '>', now()->subMonths(6)) ->where('last_active_at', '>', now()->subMonths(6))
->pluck('profile_id') ->pluck('profile_id')
->toArray(); ->toArray();
$newProfiles = User::select('profile_id', 'last_active_at', 'created_at') $newProfiles = User::select('profile_id', 'last_active_at', 'created_at')
->whereNull('last_active_at') ->whereNull('last_active_at')
->where('created_at', '>', now()->subMonths(6)) ->where('created_at', '>', now()->subMonths(6))
->pluck('profile_id') ->pluck('profile_id')
->toArray(); ->toArray();
$count = $count->merge($newProfiles); $count = $count->merge($newProfiles);
$count = $count->merge($profiles); $count = $count->merge($profiles);
return $count->unique()->count(); return $count->unique()->count();
}); });
$activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(2), function() { $activeMonth = Cache::remember('api:nodeinfo:am', now()->addHours(2), function() {
return User::select('last_active_at') return User::select('last_active_at')
->where('last_active_at', '>', now()->subMonths(1)) ->where('last_active_at', '>', now()->subMonths(1))
->orWhere('created_at', '>', now()->subMonths(1)) ->orWhere('created_at', '>', now()->subMonths(1))
->count(); ->count();
}); });
return [ return [
'metadata' => [ 'metadata' => [
'nodeName' => config('pixelfed.domain.app'), 'nodeName' => config_cache('app.name'),
'software' => [ 'software' => [
'homepage' => 'https://pixelfed.org', 'homepage' => 'https://pixelfed.org',
'repo' => 'https://github.com/pixelfed/pixelfed', 'repo' => 'https://github.com/pixelfed/pixelfed',
], ],
'config' => \App\Util\Site\Config::get() 'config' => \App\Util\Site\Config::get()
], ],
'protocols' => [ 'protocols' => [
'activitypub', 'activitypub',
], ],
'services' => [ 'services' => [
'inbound' => [], 'inbound' => [],
'outbound' => [], 'outbound' => [],
], ],
'software' => [ 'software' => [
'name' => 'pixelfed', 'name' => 'pixelfed',
'version' => config('pixelfed.version'), 'version' => config('pixelfed.version'),
], ],
'usage' => [ 'usage' => [
'localPosts' => Status::whereLocal(true)->count(), 'localPosts' => Status::whereLocal(true)->count(),
'localComments' => 0, 'localComments' => 0,
'users' => [ 'users' => [
'total' => User::count(), 'total' => User::count(),
'activeHalfyear' => (int) $activeHalfYear, 'activeHalfyear' => (int) $activeHalfYear,
'activeMonth' => (int) $activeMonth, 'activeMonth' => (int) $activeMonth,
], ],
], ],
'version' => '2.0', 'version' => '2.0',
]; ];
}); });
$res['openRegistrations'] = config('pixelfed.open_registration'); $res['openRegistrations'] = (bool) config_cache('pixelfed.open_registration');
return $res; return $res;
} }
public static function wellKnown() public static function wellKnown()
@ -86,4 +86,4 @@ class Nodeinfo {
]; ];
} }
} }

9
app/helpers.php Normal file
View file

@ -0,0 +1,9 @@
<?php
use App\Services\ConfigCacheService;
if (!function_exists('config_cache')) {
function config_cache($key) {
return ConfigCacheService::get($key);
}
}

View file

@ -1,93 +1,96 @@
{ {
"name": "pixelfed/pixelfed", "name": "pixelfed/pixelfed",
"description": "Open and ethical photo sharing platform, powered by ActivityPub federation.", "description": "Open and ethical photo sharing platform, powered by ActivityPub federation.",
"keywords": ["framework", "laravel", "pixelfed", "activitypub", "social", "network", "federation"], "keywords": ["framework", "laravel", "pixelfed", "activitypub", "social", "network", "federation"],
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "project", "type": "project",
"require": { "require": {
"php": "^7.3", "php": "^7.3",
"ext-bcmath": "*", "ext-bcmath": "*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-openssl": "*", "ext-openssl": "*",
"beyondcode/laravel-self-diagnosis": "^1.0.2", "beyondcode/laravel-self-diagnosis": "^1.0.2",
"brick/math": "^0.8", "brick/math": "^0.8",
"buzz/laravel-h-captcha": "1.0.2", "buzz/laravel-h-captcha": "1.0.2",
"doctrine/dbal": "^2.7", "doctrine/dbal": "^2.7",
"fideloper/proxy": "^4.0", "fideloper/proxy": "^4.0",
"fruitcake/laravel-cors": "^2.0", "fruitcake/laravel-cors": "^2.0",
"intervention/image": "^2.4", "intervention/image": "^2.4",
"jenssegers/agent": "^2.6", "jenssegers/agent": "^2.6",
"laravel/framework": "^8.0", "laravel/framework": "^8.0",
"laravel/helpers": "^1.1", "laravel/helpers": "^1.1",
"laravel/horizon": "^5.0", "laravel/horizon": "^5.0",
"laravel/passport": "^10.0", "laravel/passport": "^10.0",
"laravel/tinker": "^2.0", "laravel/tinker": "^2.0",
"laravel/ui": "^2.0", "laravel/ui": "^2.0",
"league/flysystem-aws-s3-v3": "~1.0", "league/flysystem-aws-s3-v3": "~1.0",
"league/flysystem-cached-adapter": "~1.0", "league/flysystem-cached-adapter": "~1.0",
"league/iso3166": "^2.1", "league/iso3166": "^2.1",
"pbmedia/laravel-ffmpeg": "^7.0", "pbmedia/laravel-ffmpeg": "^7.0",
"phpseclib/phpseclib": "~2.0", "phpseclib/phpseclib": "~2.0",
"bacon/bacon-qr-code": "^2.0.3", "bacon/bacon-qr-code": "^2.0.3",
"pixelfed/fractal": "^0.18.0", "pixelfed/fractal": "^0.18.0",
"pragmarx/google2fa": "^8.0", "pragmarx/google2fa": "^8.0",
"pixelfed/laravel-snowflake": "^2.0", "pixelfed/laravel-snowflake": "^2.0",
"pixelfed/zttp": "^0.4", "pixelfed/zttp": "^0.4",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"spatie/laravel-backup": "^6.0.0", "spatie/laravel-backup": "^6.0.0",
"spatie/laravel-image-optimizer": "^1.1", "spatie/laravel-image-optimizer": "^1.1",
"stevebauman/purify": "3.0.*", "stevebauman/purify": "3.0.*",
"symfony/http-kernel": "5.1.5" "symfony/http-kernel": "5.1.5"
}, },
"require-dev": { "require-dev": {
"brianium/paratest": "^6.1", "brianium/paratest": "^6.1",
"facade/ignition": "^2.3.6", "facade/ignition": "^2.3.6",
"fzaninotto/faker": "^1.4", "fzaninotto/faker": "^1.4",
"mockery/mockery": "^1.0", "mockery/mockery": "^1.0",
"nunomaduro/collision": "^5.0", "nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.0" "phpunit/phpunit": "^9.0"
}, },
"autoload": { "autoload": {
"classmap": [ "classmap": [
"database/seeds", "database/seeds",
"database/factories" "database/factories"
], ],
"psr-4": { "psr-4": {
"App\\": "app/" "App\\": "app/"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Tests\\": "tests/" "Tests\\": "tests/"
} },
}, "files": [
"extra": { "app/helpers.php"
"laravel": { ]
"dont-discover": [ },
] "extra": {
} "laravel": {
}, "dont-discover": [
"scripts": { ]
"post-root-package-install": [ }
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" },
], "scripts": {
"post-create-project-cmd": [ "post-root-package-install": [
"@php artisan key:generate --ansi" "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
], ],
"post-autoload-dump": [ "post-create-project-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan key:generate --ansi"
"@php artisan package:discover --ansi" ],
] "post-autoload-dump": [
}, "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"config": { "@php artisan package:discover --ansi"
"preferred-install": "dist", ]
"sort-packages": true, },
"optimize-autoloader": true "config": {
}, "preferred-install": "dist",
"minimum-stability": "dev", "sort-packages": true,
"prefer-stable": true "optimize-autoloader": true
},
"minimum-stability": "dev",
"prefer-stable": true
} }

View file

@ -108,6 +108,11 @@ return [
'cipher' => 'AES-256-CBC', 'cipher' => 'AES-256-CBC',
'short_description' => 'Pixelfed - Photo sharing for everyone',
'description' => 'Pixelfed - Photo sharing for everyone',
'rules' => null,
'logo' => '/img/pixelfed-icon-color.svg',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Autoloaded Service Providers | Autoloaded Service Providers

View file

@ -72,4 +72,6 @@ return [
'org' => env('COVID_LABEL_ORG', 'visit the WHO website') 'org' => env('COVID_LABEL_ORG', 'visit the WHO website')
] ]
], ],
'enable_cc' => env('ENABLE_CONFIG_CACHE', false)
]; ];

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateConfigCachesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('config_cache', function (Blueprint $table) {
$table->id();
$table->string('k')->unique()->index();
$table->text('v')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('config_cache');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserPronounsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_pronouns', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id')->nullable()->unique()->index();
$table->bigInteger('profile_id')->unique()->index();
$table->json('pronouns')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_pronouns');
}
}

BIN
public/js/profile.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -145,7 +145,8 @@
</div> </div>
</div> </div>
<p class="mb-0 d-flex align-items-center"> <p class="mb-0 d-flex align-items-center">
<span class="font-weight-bold pr-3">{{profile.display_name}}</span> <span class="font-weight-bold mr-1">{{profile.display_name}}</span>
<span v-if="profile.pronouns" class="text-muted small">{{profile.pronouns.join('/')}}</span>
</p> </p>
<div v-if="profile.note" class="mb-0" v-html="profile.note"></div> <div v-if="profile.note" class="mb-0" v-html="profile.note"></div>
<p v-if="profile.website" class=""><a :href="profile.website" class="profile-website" rel="me external nofollow noopener" target="_blank" @click.prevent="remoteRedirect(profile.website)">{{truncate(profile.website,24)}}</a></p> <p v-if="profile.website" class=""><a :href="profile.website" class="profile-website" rel="me external nofollow noopener" target="_blank" @click.prevent="remoteRedirect(profile.website)">{{truncate(profile.website,24)}}</a></p>
@ -387,6 +388,7 @@
</div> </div>
</div> </div>
</div> </div>
<b-modal <b-modal
v-if="profile && following" v-if="profile && following"
ref="followingModal" ref="followingModal"

View file

@ -1,67 +1,92 @@
<nav class="navbar navbar-expand-lg navbar-light"> <nav class="navbar navbar-expand-lg navbar-light">
<div class="container"> <div class="container">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topbarNav" aria-controls="topbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topbarNav" aria-controls="topbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="topbarNav"> <div class="collapse navbar-collapse" id="topbarNav">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item mx-2 {{request()->is('*admin/dashboard')?'active':''}}"> <li class="nav-item mx-4 {{request()->is('*admin/dashboard')?'active':''}}">
<a class="nav-link" href="{{route('admin.home')}}">Dashboard</a> <a class="nav-link" href="{{route('admin.home')}}">Dashboard</a>
</li> </li>
<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}"> {{--<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Messages</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Configuration</a>
</li> </li>
<li class="nav-item mx-2 {{request()->is('*instances*')?'active':''}}"> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.instances')}}">Instances</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Content</a>
</li> </li>
<li class="nav-item mx-2 {{request()->is('*media*')?'active':''}}"> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.media')}}">Media</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Federation</a>
</li> </li>
<li class="nav-item mx-2 {{request()->is('*reports*')?'active':''}}"> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.reports')}}">Moderation</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Moderation</a>
</li> </li>
<li class="nav-item mx-2 {{request()->is('*profiles*')?'active':''}}"> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.profiles')}}">Profiles</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Platform</a>
</li> </li>
<li class="nav-item mx-2 {{request()->is('*statuses*')?'active':''}}"> <li class="nav-item mx-2 align-self-center text-lighter">|</li>
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.statuses')}}">Statuses</a> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
</li> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Media</a>
<li class="nav-item mx-2 {{request()->is('*users*')?'active':''}}"> </li>
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.users')}}">Users</a> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
</li> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Profiles</a>
<li class="nav-item dropdown mx-3 {{request()->is(['*settings*','*discover*', '*site-news*'])?'active':''}}"> </li>
<a class="nav-link dropdown-toggle px-4" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
More <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Statuses</a>
</a> </li> --}}
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <li class="nav-item mx-4 {{request()->is('*messages*')?'active':''}}">
<a class="dropdown-item font-weight-bold {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}">Apps</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Messages</a>
<a class="dropdown-item font-weight-bold {{request()->is('*discover*')?'active':''}}" href="{{route('admin.discover')}}">Discover</a> </li>
<a class="dropdown-item font-weight-bold {{request()->is('*hashtags*')?'active':''}}" href="{{route('admin.hashtags')}}">Hashtags</a> {{-- <li class="nav-item mx-4 {{request()->is('*instances*')?'active':''}}">
<a class="dropdown-item font-weight-bold {{request()->is('*site-news*')?'active':''}}" href="/i/admin/site-news">Newsroom</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.instances')}}">Instances</a>
<div class="dropdown-divider"></div> </li> --}}
<a class="dropdown-item font-weight-bold" href="/horizon">Horizon</a> <li class="nav-item mx-4 {{request()->is('*reports*')?'active':''}}">
{{-- <a class="dropdown-item font-weight-bold" href="#">Websockets</a> --}} <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.reports')}}">Moderation</a>
<div class="dropdown-divider"></div> </li>
<a class="dropdown-item font-weight-bold {{request()->is('*settings*')?'active':''}}" href="{{route('admin.settings')}}">Settings</a> {{-- <li class="nav-item mx-2 {{request()->is('*profiles*')?'active':''}}">
</div> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.profiles')}}">Profiles</a>
</li> </li> --}}
</ul> <li class="nav-item mx-4 {{request()->is('*statuses*')?'active':''}}">
</div> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.statuses')}}">Statuses</a>
</div> </li>
<li class="nav-item mx-4 {{request()->is('*users*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.users')}}">Users</a>
</li>
<li class="nav-item mx-4 {{request()->is('*settings*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.settings')}}">Settings</a>
</li>
<li class="nav-item dropdown ml-3 {{request()->is(['*discover*', '*site-news*'])?'active':''}}">
<a class="nav-link dropdown-toggle px-4" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
More
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item font-weight-bold {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}">Apps</a>
{{-- <a class="dropdown-item font-weight-bold {{request()->is('*discover*')?'active':''}}" href="{{route('admin.discover')}}">Discover</a> --}}
<a class="dropdown-item font-weight-bold {{request()->is('*hashtags*')?'active':''}}" href="{{route('admin.hashtags')}}">Hashtags</a>
<a class="dropdown-item font-weight-bold {{request()->is('*instances*')?'active':''}}" href="{{route('admin.instances')}}">Instances</a>
<a class="dropdown-item font-weight-bold {{request()->is('*media*')?'active':''}}" href="{{route('admin.media')}}">Media</a>
<a class="dropdown-item font-weight-bold {{request()->is('*site-news*')?'active':''}}" href="/i/admin/site-news">Newsroom</a>
<a class="dropdown-item font-weight-bold {{request()->is('*profiles*')?'active':''}}" href="/i/admin/profiles">Profiles</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="/horizon">Horizon</a>
</div>
</li>
</ul>
</div>
</div>
</nav> </nav>
@push('styles') @push('styles')
<style type="text/css"> <style type="text/css">
#topbarNav .nav-item:hover { #topbarNav .nav-item:hover {
border-bottom: 2px solid #08d; border-bottom: 2px solid #08d;
margin-bottom: -7px; margin-bottom: -7px;
} }
#topbarNav .nav-item.active { #topbarNav .nav-item.active {
border-bottom: 2px solid #08d; border-bottom: 2px solid #08d;
margin-bottom: -7px; margin-bottom: -7px;
} }
#topbarNav .nav-item.active .nav-link { #topbarNav .nav-item.active .nav-link {
font-weight: bold !important; font-weight: bold !important;
} }
</style> </style>
@endpush @endpush

View file

@ -3,61 +3,181 @@
@include('admin.settings.sidebar') @include('admin.settings.sidebar')
@section('section') @section('section')
<div class="title"> <div class="title mb-4">
<h3 class="font-weight-bold">Settings</h3> <h3 class="font-weight-bold">Settings</h3>
</div> @if(config('instance.enable_cc'))
<hr> <p class="lead mb-0">Manage instance settings.</p>
<form method="post"> <p class="mb-0"><span class="font-weight-bold">Warning</span>: These settings will override .env variables</p>
@csrf </div>
<div class="form-group row"> <form method="post">
<label for="app_name" class="col-sm-3 col-form-label font-weight-bold text-right">App Name</label> @csrf
<div class="col-sm-9"> <ul class="nav nav-tabs nav-fill border-bottom-0" id="myTab" role="tablist">
<input type="text" class="form-control" id="app_name" name="APP_NAME" placeholder="Application Name ex: pixelfed" value="{{config('app.name')}}" autocomplete="off"> <li class="nav-item">
<p class="text-muted small help-text font-weight-bold mb-0">Site name, default: pixelfed</p> <a class="nav-link font-weight-bold px-4 active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">General</a>
</div> </li>
</div> <li class="nav-item border-none">
<div class="form-group row"> <a class="nav-link font-weight-bold px-4" id="media-tab" data-toggle="tab" href="#media" role="tab" aria-controls="media">Media</a>
<label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">App URL</label> </li>
<div class="col-sm-9"> <li class="nav-item border-none">
<input type="text" class="form-control" id="app_url" name="APP_URL" placeholder="Application URL" value="{{config('app.url')}}"> <a class="nav-link font-weight-bold px-4" id="users-tab" data-toggle="tab" href="#users" role="tab" aria-controls="users">Users</a>
<p class="text-muted small help-text font-weight-bold mb-0">App URL, used for building URLs ex: https://example.org</p> </li>
</div> <li class="nav-item">
</div> <a class="nav-link font-weight-bold px-4" id="advanced-tab" data-toggle="tab" href="#advanced" role="tab" aria-controls="advanced">Advanced</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="form-group row"> <div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">App Domain</label> <div class="form-group mb-0">
<div class="col-sm-9"> <div class="ml-n4 mr-n2 p-3 bg-light border-top">
<input type="text" class="form-control" id="app_url" name="app_domain" placeholder="example.org" value="{{config('pixelfed.domain.app')}}"> <label class="font-weight-bold text-muted">Manage Core Features</label>
<p class="text-muted small help-text font-weight-bold mb-0">Used for routing ex: example.org</p> <div class="custom-control custom-checkbox mt-2">
</div> <input type="checkbox" name="activitypub" class="custom-control-input" id="ap" {{config_cache('federation.activitypub.enabled') ? 'checked' : ''}}>
</div> <label class="custom-control-label font-weight-bold" for="ap">ActivityPub</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="open_registration" class="custom-control-input" id="openReg" {{config_cache('pixelfed.open_registration') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="openReg">Open Registrations</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="mobile_apis" class="custom-control-input" id="cf2" {{config_cache('pixelfed.oauth_enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf2">Mobile APIs</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="stories" class="custom-control-input" id="cf3" {{config_cache('instance.stories.enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf3">Stories</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="ig_import" class="custom-control-input" id="cf4" {{config_cache('pixelfed.import.instagram.enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf4">Instagram Import</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="spam_detection" class="custom-control-input" id="cf5" {{config_cache('pixelfed.bouncer.enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf5">Spam detection</label>
</div>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
<label class="font-weight-bold text-muted">Name</label>
<input class="form-control col-8" name="name" placeholder="Pixelfed" value="{{config_cache('app.name')}}">
<p class="help-text small text-muted mt-3 mb-0">The instance name used in titles, metadata and apis.</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 bg-light border-bottom">
<label class="font-weight-bold text-muted">Short Description</label>
<textarea class="form-control" rows="3" name="short_description">{{config_cache('app.short_description')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Short description of instance used on various pages and apis.</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 bg-light border-bottom">
<label class="font-weight-bold text-muted">Long Description</label>
<textarea class="form-control" rows="3" name="long_description">{{config_cache('app.description')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Longer description of instance used on about page.</p>
</div>
</div>
</div>
<div class="tab-pane" id="users" role="tabpanel" aria-labelledby="users-tab">
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 bg-light border-top">
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="require_email_verification" class="custom-control-input" id="mailVerification" {{config_cache('pixelfed.enforce_email_verification') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="mailVerification">Require Email Verification</label>
</div>
</div>
</div>
<div class="form-group">
<div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
<div class="custom-control custom-checkbox my-2">
<input type="checkbox" name="enforce_account_limit" class="custom-control-input" id="userEnforceLimit" {{config_cache('pixelfed.enforce_account_limit') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="userEnforceLimit">Enable account storage limit</label>
<p class="help-text small text-muted">Set a storage limit per user account.</p>
</div>
<label class="font-weight-bold text-muted">Account Limit</label>
<input class="form-control" name="account_limit" placeholder="Pixelfed" value="{{config_cache('pixelfed.max_account_size')}}">
<p class="help-text small text-muted mt-3 mb-0">Account limit size in KB.</p>
<p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_account_size')}} KB = {{floor(config_cache('pixelfed.max_account_size') / 1024)}} MB</p>
</div>
</div>
</div>
<div class="form-group row"> <div class="tab-pane" id="media" role="tabpanel" aria-labelledby="media-tab">
<label for="app_url" class="col-sm-3 col-form-label font-weight-bold text-right">Admin Domain</label> <div class="form-group mb-0">
<div class="col-sm-9"> <div class="ml-n4 mr-n2 p-3 bg-light border-top">
<input type="text" class="form-control" id="admin_domain" name="admin_domain" placeholder="admin.example.org" value="{{config('pixelfed.domain.admin')}}"> <label class="font-weight-bold text-muted">Max Size</label>
<p class="text-muted small help-text font-weight-bold mb-0">Used for routing the admin dashboard ex: admin.example.org</p> <input class="form-control" name="max_photo_size" value="{{config_cache('pixelfed.max_photo_size')}}">
</div> <p class="help-text small text-muted mt-3 mb-0">Maximum file upload size in KB</p>
</div> <p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_photo_size')}} KB = {{number_format(config_cache('pixelfed.max_photo_size') / 1024)}} MB</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 bg-light border-top">
<label class="font-weight-bold text-muted">Photo Album Limit</label>
<input class="form-control" name="max_album_length" value="{{config_cache('pixelfed.max_album_length')}}">
<p class="help-text small text-muted mt-3 mb-0">The maximum number of photos or videos per album</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 bg-light border-top">
<label class="font-weight-bold text-muted">Image Quality</label>
<input class="form-control" name="image_quality" value="{{config_cache('pixelfed.image_quality')}}">
<p class="help-text small text-muted mt-3 mb-0">Image optimization quality from 0-100%. Set to 0 to disable image optimization.</p>
</div>
</div>
<div class="form-group">
<div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
<label class="font-weight-bold text-muted">Media Types</label>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_jpeg" class="custom-control-input" id="mediaType1" {{$jpeg ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType1">Allow <span class="border border-dark px-1 rounded font-weight-bold">JPEG</span> images (image/jpg)</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_png" class="custom-control-input" id="mediaType2" {{$png ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType2">Allow <span class="border border-dark px-1 rounded font-weight-bold">PNG</span> images (image/png)</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_gif" class="custom-control-input" id="mediaType3" {{$gif ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType3">Allow <span class="border border-dark px-1 rounded font-weight-bold">GIF</span> images (image/gif)</label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_mp4" class="custom-control-input" id="mediaType4" {{$mp4 ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType4">Allow <span class="border border-dark px-1 rounded font-weight-bold">MP4</span> video (video/mp4)</label>
</div>
<p class="help-text small text-muted mt-3 mb-0">Allowed media types.</p>
</div>
</div>
</div>
{{-- <div class="alert alert-info border-0"> <div class="tab-pane" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
<div class="media d-flex align-items-center"> <div class="form-group mb-0">
<div class="mr-3"> <div class="ml-n4 mr-n2 p-3 bg-light border-top border-bottom">
<i class="fas fa-info-circle fa-2x"></i> <label class="font-weight-bold text-muted">Custom CSS</label>
</div> <div class="custom-control custom-checkbox my-2">
<div class="media-body"> <input type="checkbox" name="show_custom_css" class="custom-control-input" id="showCustomCss" {{config_cache('uikit.show_custom.css') ? 'checked' : ''}}>
<p class="mb-0 lead">Tip:</p> <label class="custom-control-label font-weight-bold" for="showCustomCss">Enable custom CSS</label>
<p class="mb-0">You can edit the .env file in the <a href="#" class="font-weight-bold">Configuration</a> settings.</p> </div>
</div> <textarea class="form-control" name="custom_css" rows="3">{{config_cache('uikit.custom.css')}}</textarea>
</div> <p class="help-text small text-muted mt-3 mb-0">Add custom CSS, will be used on all pages</p>
</div> </div>
</div>
</div>
<hr> </div>
<div class="form-group row mb-0">
<div class="col-12 text-right"> <div class="form-group row mb-0 mt-4">
<button type="submit" class="btn btn-primary font-weight-bold">Submit</button> <div class="col-12 text-right">
</div> <button type="submit" class="btn btn-primary font-weight-bold px-5">Save</button>
</div> --}} </div>
</form> </div>
@endsection </form>
@else
</div>
<div class="py-5">
<p class="lead text-center font-weight-bold">Not enabled</p>
<p class="text-center">Add <code>ENABLE_CONFIG_CACHE=true</code> in your <span class="font-weight-bold">.env</span> file <br /> and run <span class="font-weight-bold">php artisan config:cache</span></p>
</div>
@endif
@endsection

View file

@ -3,26 +3,29 @@
<li class="nav-item pl-3"> <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings') ? 'font-weight-bold':''}}" href="{{route('admin.settings')}}">Home</a> <a class="nav-link text-muted {{request()->is('*settings') ? 'font-weight-bold':''}}" href="{{route('admin.settings')}}">Home</a>
</li> </li>
<li class="nav-item pl-3"> {{-- <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/backups') ? 'font-weight-bold':''}}" href="{{route('admin.settings.backups')}}">Backups</a> <a class="nav-link text-muted {{request()->is('*settings/backups') ? 'font-weight-bold':''}}" href="{{route('admin.settings.backups')}}">Backups</a>
</li> </li> --}}
<li class="nav-item pl-3"> {{-- <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/config') ? 'font-weight-bold':''}}" href="{{route('admin.settings.config')}}">Configuration</a> <a class="nav-link text-muted {{request()->is('*settings/config') ? 'font-weight-bold':''}}" href="{{route('admin.settings.config')}}">Configuration</a>
</li> </li> --}}
<li class="nav-item pl-3"> {{-- <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/customize') ? 'font-weight-bold':''}}" href="{{route('admin.settings.customize')}}">Customize</a>
</li> --}}
{{-- <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/features') ? 'font-weight-bold':''}}" href="{{route('admin.settings.features')}}">Features</a> <a class="nav-link text-muted {{request()->is('*settings/features') ? 'font-weight-bold':''}}" href="{{route('admin.settings.features')}}">Features</a>
</li> </li> --}}
<li class="nav-item pl-3"> {{-- <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/maintenance') ? 'font-weight-bold':''}}" href="{{route('admin.settings.maintenance')}}">Maintenance</a> <a class="nav-link text-muted {{request()->is('*settings/maintenance') ? 'font-weight-bold':''}}" href="{{route('admin.settings.maintenance')}}">Maintenance</a>
</li> </li> --}}
<li class="nav-item pl-3"> <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/page*') ? 'font-weight-bold':''}}" href="{{route('admin.settings.pages')}}">Pages</a> <a class="nav-link text-muted {{request()->is('*settings/page*') ? 'font-weight-bold':''}}" href="{{route('admin.settings.pages')}}">Pages</a>
</li> </li>
<li class="nav-item pl-3"> {{-- <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/storage') ? 'font-weight-bold':''}}" href="{{route('admin.settings.storage')}}">Storage</a> <a class="nav-link text-muted {{request()->is('*settings/storage') ? 'font-weight-bold':''}}" href="{{route('admin.settings.storage')}}">Storage</a>
</li> </li> --}}
<li class="nav-item pl-3"> <li class="nav-item pl-3">
<a class="nav-link text-muted {{request()->is('*settings/system') ? 'font-weight-bold':''}}" href="{{route('admin.settings.system')}}">System</a> <a class="nav-link text-muted {{request()->is('*settings/system') ? 'font-weight-bold':''}}" href="{{route('admin.settings.system')}}">System</a>
</li> </li>
</ul> </ul>
@endsection @endsection

View file

@ -90,7 +90,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">storage used</th> <th scope="row" class="font-weight-bold text-muted text-uppercase pl-3 small" style="line-height: 2;">storage used</th>
<td class="text-right font-weight-bold">{{PrettyNumber::size($profile->media()->sum('size'))}}<span class="text-muted"> / {{PrettyNumber::size(config('pixelfed.max_account_size') * 1000)}}</span></td> <td class="text-right font-weight-bold">{{PrettyNumber::size($profile->media()->sum('size'))}}<span class="text-muted"> / {{PrettyNumber::size(config_cache('pixelfed.max_account_size') * 1000)}}</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -118,4 +118,4 @@
</div> </div>
</div> </div>
</div> </div>
@endsection @endsection

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ app()->getLocale() }}"> <html lang="{{ app()->getLocale() }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -9,10 +9,10 @@
<meta name="robots" content="noimageindex, noarchive"> <meta name="robots" content="noimageindex, noarchive">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<title>{{ $title ?? config('app.name', 'Laravel') }}</title> <title>{{ $title ?? config_cache('app.name') }}</title>
@if(isset($title))<meta property="og:site_name" content="{{ config('app.name', 'Laravel') }}"> @if(isset($title))<meta property="og:site_name" content="{{ config_cache('app.name') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'Laravel') }}"> <meta property="og:title" content="{{ $title ?? config_cache('app.name') }}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="{{request()->url()}}"> <meta property="og:url" content="{{request()->url()}}">
@endif @endif

View file

@ -2,82 +2,86 @@
@auth @auth
<html lang="{{ app()->getLocale() }}"> <html lang="{{ app()->getLocale() }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title> <title>{{ $title ?? config_cache('app.name') }}</title>
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}"> <meta property="og:site_name" content="{{ config_cache('app.name') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}"> <meta property="og:title" content="{{ $title ?? config_cache('app.name') }}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="{{url(request()->url())}}"> <meta property="og:url" content="{{url(request()->url())}}">
@stack('meta') @stack('meta')
<meta name="medium" content="image">
<meta name="theme-color" content="#10c5f8">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="canonical" href="{{url(request()->url())}}">
@if(request()->cookie('dark-mode'))
<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
@else
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@endif
@stack('styles')
@if(config_cache('uikit.show_custom.css'))
<style type="text/css">{!!config_cache('uikit.custom.css')!!}</style>
@endif
<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
<meta name="medium" content="image">
<meta name="theme-color" content="#10c5f8">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="canonical" href="{{url(request()->url())}}">
@if(request()->cookie('dark-mode'))
<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
@else
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@endif
@stack('styles')
<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
</head> </head>
<body class="loggedIn"> <body class="loggedIn">
@include('layouts.partial.nav') @include('layouts.partial.nav')
<main id="content"> <main id="content">
@yield('content') @yield('content')
<noscript> <noscript>
<div class="container"> <div class="container">
<p class="pt-5 text-center lead">Please enable javascript to view this content.</p> <p class="pt-5 text-center lead">Please enable javascript to view this content.</p>
</div> </div>
</noscript> </noscript>
</main> </main>
@include('layouts.partial.footer') @include('layouts.partial.footer')
<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script> <script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script> <script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/app.js') }}"></script> <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/components.js') }}"></script> <script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
@stack('scripts') @stack('scripts')
<div class="d-block d-sm-none mt-5"></div> <div class="mobile-footer-spacer d-block d-sm-none mt-5"></div>
<div class="d-block d-sm-none fixed-bottom"> <div class="mobile-footer d-block d-sm-none fixed-bottom">
<div class="card card-body rounded-0 py-2 box-shadow" style="border-top:1px solid #F1F5F8"> <div class="card card-body rounded-0 py-2 box-shadow" style="border-top:1px solid #F1F5F8">
<ul class="nav nav-pills nav-fill d-flex align-items-middle"> <ul class="nav nav-pills nav-fill d-flex align-items-middle">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/"><i class="fas fa-home fa-lg"></i></a> <a class="nav-link text-dark" href="/"><i class="fas fa-home fa-lg"></i></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/discover"><i class="fas fa-search fa-lg"></i></a> <a class="nav-link text-dark" href="/discover"><i class="fas fa-search fa-lg"></i></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<div class="nav-link cursor-pointer text-dark" onclick="App.util.compose.post()"> <div class="nav-link cursor-pointer text-dark" onclick="App.util.compose.post()">
<i class="far fa-plus-square fa-2x"></i> <i class="far fa-plus-square fa-2x"></i>
</div> </div>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/account/activity"><i class="far fa-bell fa-lg"></i></a> <a class="nav-link text-dark" href="/account/activity"><i class="far fa-bell fa-lg"></i></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" href="/i/me"><i class="far fa-user fa-lg"></i></a> <a class="nav-link text-dark" href="/i/me"><i class="far fa-user fa-lg"></i></a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</body> </body>
</html> </html>
@endauth @endauth
@ -85,41 +89,41 @@
@guest @guest
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title> <title>{{ $title ?? config('app.name', 'Pixelfed') }}</title>
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}"> <meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}"> <meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="{{url(request()->url())}}"> <meta property="og:url" content="{{url(request()->url())}}">
@stack('meta') @stack('meta')
<meta name="medium" content="image"> <meta name="medium" content="image">
<meta name="theme-color" content="#10c5f8"> <meta name="theme-color" content="#10c5f8">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2"> <link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2"> <link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="canonical" href="{{url(request()->url())}}"> <link rel="canonical" href="{{url(request()->url())}}">
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light"> <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script> <script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
@stack('styles') @stack('styles')
</head> </head>
<body> <body>
@include('layouts.partial.nav') @include('layouts.partial.nav')
<main id="content"> <main id="content">
@yield('content') @yield('content')
</main> </main>
@include('layouts.partial.footer') @include('layouts.partial.footer')
<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script> <script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script> <script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/app.js') }}"></script> <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/components.js') }}"></script> <script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
@stack('scripts') @stack('scripts')
</body> </body>
</html> </html>
@endguest @endguest

View file

@ -1,49 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ app()->getLocale() }}"> <html lang="{{ app()->getLocale() }}">
<head> <head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="mobile-web-app-capable" content="yes"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? config('app.name', 'Laravel') }}</title> <meta name="mobile-web-app-capable" content="yes">
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}"> <title>{{ $title ?? config_cache('app.name') }}</title>
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{request()->url()}}">
@stack('meta')
<meta name="medium" content="image"> <meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}">
<meta name="theme-color" content="#10c5f8"> <meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta property="og:type" content="article">
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2"> <meta property="og:url" content="{{request()->url()}}">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2"> @stack('meta')
<link rel="canonical" href="{{request()->url()}}">
@if(request()->cookie('dark-mode'))
<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
@else
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@endif
@stack('styles')
<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script> <meta name="medium" content="image">
<meta name="theme-color" content="#10c5f8">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
<link rel="canonical" href="{{request()->url()}}">
@if(request()->cookie('dark-mode'))
<link href="{{ mix('css/appdark.css') }}" rel="stylesheet" data-stylesheet="dark">
@else
<link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
@endif
@stack('styles')
<script type="text/javascript">window._sharedData = {curUser: {}, version: 0}; window.App = {config: {!!App\Util\Site\Config::json()!!}};</script>
</head> </head>
<body class="w-100 h-100"> <body class="w-100 h-100">
<main id="content" class="w-100 h-100"> <main id="content" class="w-100 h-100">
@yield('content') @yield('content')
</main> </main>
<script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script> <script type="text/javascript" src="{{ mix('js/manifest.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script> <script type="text/javascript" src="{{ mix('js/vendor.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/app.js') }}"></script> <script type="text/javascript" src="{{ mix('js/app.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/components.js') }}"></script> <script type="text/javascript" src="{{ mix('js/components.js') }}"></script>
@stack('scripts') @stack('scripts')
</body> </body>
</html> </html>

View file

@ -2,7 +2,7 @@
<div class="container"> <div class="container">
<a class="navbar-brand d-flex align-items-center" href="{{ route('timeline.personal') }}" title="Logo"> <a class="navbar-brand d-flex align-items-center" href="{{ route('timeline.personal') }}" title="Logo">
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager" alt="Pixelfed logo"> <img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" loading="eager" alt="Pixelfed logo">
<span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config('app.name', 'pixelfed') }}</span> <span class="font-weight-bold mb-0 d-none d-sm-block" style="font-size:20px;">{{ config_cache('app.name') }}</span>
</a> </a>
<div class="collapse navbar-collapse"> <div class="collapse navbar-collapse">
@ -22,7 +22,7 @@
{{ __('Login') }} {{ __('Login') }}
</a> </a>
</li> </li>
@if(config('pixelfed.open_registration') && config('instance.restricted.enabled') == false) @if(config_cache('pixelfed.open_registration') && config('instance.restricted.enabled') == false)
<li> <li>
<a class="ml-3 nav-link font-weight-bold text-dark" href="{{ route('register') }}" title="Register"> <a class="ml-3 nav-link font-weight-bold text-dark" href="{{ route('register') }}" title="Register">
{{ __('Register') }} {{ __('Register') }}

View file

@ -6,7 +6,7 @@
<h3 class="font-weight-bold">Applications</h3> <h3 class="font-weight-bold">Applications</h3>
</div> </div>
<hr> <hr>
@if(config('pixelfed.oauth_enabled') == true) @if(config_cache('pixelfed.oauth_enabled') == true)
<passport-authorized-clients></passport-authorized-clients> <passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens> <passport-personal-access-tokens></passport-personal-access-tokens>
@else @else
@ -16,4 +16,4 @@
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{mix('js/developers.js')}}"></script> <script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
@endpush @endpush

View file

@ -6,7 +6,7 @@
<h3 class="font-weight-bold">Developers</h3> <h3 class="font-weight-bold">Developers</h3>
</div> </div>
<hr> <hr>
@if(config('pixelfed.oauth_enabled') == true) @if(config_cache('pixelfed.oauth_enabled') == true)
<passport-clients></passport-clients> <passport-clients></passport-clients>
@else @else
<p class="lead">OAuth has not been enabled on this instance.</p> <p class="lead">OAuth has not been enabled on this instance.</p>
@ -16,4 +16,4 @@
@push('scripts') @push('scripts')
<script type="text/javascript" src="{{mix('js/developers.js')}}"></script> <script type="text/javascript" src="{{mix('js/developers.js')}}"></script>
@endpush @endpush

View file

@ -2,99 +2,111 @@
@section('section') @section('section')
<div class="title"> <div class="title">
<h3 class="font-weight-bold">Account Settings</h3> <h3 class="font-weight-bold">Account Settings</h3>
</div> </div>
<hr> <hr>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-3"> <div class="col-sm-3">
<img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" height="38px" class="rounded-circle float-right"> <img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" height="38px" class="rounded-circle float-right">
</div> </div>
<div class="col-sm-9"> <div class="col-sm-9">
<p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p> <p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p>
<p class=""> <p class="">
<a href="#" class="font-weight-bold change-profile-photo" data-toggle="collapse" data-target="#avatarCollapse" aria-expanded="false" aria-controls="avatarCollapse">Change Profile Photo</a> <a href="#" class="font-weight-bold change-profile-photo" data-toggle="collapse" data-target="#avatarCollapse" aria-expanded="false" aria-controls="avatarCollapse">Change Profile Photo</a>
</p> </p>
<div class="collapse" id="avatarCollapse"> <div class="collapse" id="avatarCollapse">
<form method="post" action="/settings/avatar" enctype="multipart/form-data"> <form method="post" action="/settings/avatar" enctype="multipart/form-data">
@csrf @csrf
<div class="card card-body"> <div class="card card-body">
<div class="custom-file mb-1"> <div class="custom-file mb-1">
<input type="file" name="avatar" class="custom-file-input" id="avatarInput"> <input type="file" name="avatar" class="custom-file-input" id="avatarInput">
<label class="custom-file-label" for="avatarInput">Select a profile photo</label> <label class="custom-file-label" for="avatarInput">Select a profile photo</label>
</div> </div>
<p><span class="small font-weight-bold">Must be a jpeg or png. Max avatar size: <span id="maxAvatarSize"></span></span></p> <p><span class="small font-weight-bold">Must be a jpeg or png. Max avatar size: <span id="maxAvatarSize"></span></span></p>
<div id="previewAvatar"></div> <div id="previewAvatar"></div>
<p class="mb-0"><button type="submit" class="btn btn-primary px-4 py-0 font-weight-bold">Upload</button></p> <p class="mb-0"><button type="submit" class="btn btn-primary px-4 py-0 font-weight-bold">Upload</button></p>
</div> </div>
</form> </form>
</div> </div>
<p class=""> <p class="">
<a class="font-weight-bold text-muted delete-profile-photo" href="#">Delete Profile Photo</a> <a class="font-weight-bold text-muted delete-profile-photo" href="#">Delete Profile Photo</a>
</p> </p>
</div> </div>
</div> </div>
<form method="post"> <form method="post">
@csrf @csrf
<div class="form-group row"> <div class="form-group row">
<label for="name" class="col-sm-3 col-form-label font-weight-bold">Name</label> <label for="name" class="col-sm-3 col-form-label font-weight-bold">Name</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" id="name" name="name" placeholder="Your Name" value="{{Auth::user()->profile->name}}" v-pre> <input type="text" class="form-control" id="name" name="name" placeholder="Your Name" value="{{Auth::user()->profile->name}}" v-pre>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="website" class="col-sm-3 col-form-label font-weight-bold">Website</label> <label for="website" class="col-sm-3 col-form-label font-weight-bold">Website</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" id="website" name="website" placeholder="Website" value="{{Auth::user()->profile->website}}" v-pre> <input type="text" class="form-control" id="website" name="website" placeholder="Website" value="{{Auth::user()->profile->website}}" v-pre>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="bio" class="col-sm-3 col-form-label font-weight-bold">Bio</label> <label for="bio" class="col-sm-3 col-form-label font-weight-bold">Bio</label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea class="form-control" id="bio" name="bio" placeholder="Add a bio here" rows="2" data-max-length="{{config('pixelfed.max_bio_length')}}" v-pre>{{Auth::user()->profile->bio}}</textarea> <textarea class="form-control" id="bio" name="bio" placeholder="Add a bio here" rows="2" data-max-length="{{config('pixelfed.max_bio_length')}}" v-pre>{{Auth::user()->profile->bio}}</textarea>
<p class="form-text"> <p class="form-text">
<span class="bio-counter float-right small text-muted">0/{{config('pixelfed.max_bio_length')}}</span> <span class="bio-counter float-right small text-muted">0/{{config('pixelfed.max_bio_length')}}</span>
</p> </p>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="language" class="col-sm-3 col-form-label font-weight-bold">Language</label> <label for="language" class="col-sm-3 col-form-label font-weight-bold">Language</label>
<div class="col-sm-9"> <div class="col-sm-9">
<select class="form-control" name="language"> <select class="form-control" name="language">
@foreach(App\Util\Localization\Localization::languages() as $lang) @foreach(App\Util\Localization\Localization::languages() as $lang)
<option value="{{$lang}}" {{(Auth::user()->language ?? 'en') == $lang ? 'selected':''}}>{{locale_get_display_language($lang, 'en')}} - {{locale_get_display_language($lang, $lang)}}</option> <option value="{{$lang}}" {{(Auth::user()->language ?? 'en') == $lang ? 'selected':''}}>{{locale_get_display_language($lang, 'en')}} - {{locale_get_display_language($lang, $lang)}}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
</div> </div>
@if(config('pixelfed.enforce_account_limit')) <div class="form-group row">
<div class="pt-3"> <label for="pronouns" class="col-sm-3 col-form-label font-weight-bold">Pronouns</label>
<p class="font-weight-bold text-muted text-center">Storage Usage</p> <div class="col-sm-9">
</div> <select class="form-control" name="pronouns[]" multiple="" id="pronouns">
<div class="form-group row"> <option>Select Pronoun(s)</option>
<label class="col-sm-3 col-form-label font-weight-bold">Storage Used</label> @foreach(\App\Services\PronounService::pronouns() as $val)
<div class="col-sm-9"> <option value="{{$val}}" {{$pronouns && in_array($val, $pronouns) ? 'selected' : ''}}>{{$val}}</option>
<div class="progress mt-2"> @endforeach
<div class="progress-bar" role="progressbar" style="width: {{$storage['percentUsed']}}%" aria-valuenow="{{$storage['percentUsed']}}" aria-valuemin="0" aria-valuemax="100"></div> </select>
</div> <p class="help-text text-muted small">Select up to 4 pronouns that will appear on your profile.</p>
<div class="help-text"> </div>
<span class="small text-muted"> </div>
{{$storage['percentUsed']}}% used @if(config_cache('pixelfed.enforce_account_limit'))
</span> <div class="pt-3">
<span class="small text-muted float-right"> <p class="font-weight-bold text-muted text-center">Storage Usage</p>
{{$storage['usedPretty']}} / {{$storage['limitPretty']}} </div>
</span> <div class="form-group row">
</div> <label class="col-sm-3 col-form-label font-weight-bold">Storage Used</label>
</div> <div class="col-sm-9">
</div> <div class="progress mt-2">
@endif <div class="progress-bar" role="progressbar" style="width: {{$storage['percentUsed']}}%" aria-valuenow="{{$storage['percentUsed']}}" aria-valuemin="0" aria-valuemax="100"></div>
<hr> </div>
<div class="form-group row"> <div class="help-text">
<div class="col-12 text-right"> <span class="small text-muted">
<button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button> {{$storage['percentUsed']}}% used
</div> </span>
</div> <span class="small text-muted float-right">
</form> {{$storage['usedPretty']}} / {{$storage['limitPretty']}}
</span>
</div>
</div>
</div>
@endif
<hr>
<div class="form-group row">
<div class="col-12 text-right">
<button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
</div>
</div>
</form>
@endsection @endsection
@ -102,72 +114,72 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
let el = $('#bio'); let el = $('#bio');
let len = el.val().length; let len = el.val().length;
let limit = el.data('max-length'); let limit = el.data('max-length');
if(len > 100) { if(len > 100) {
el.attr('rows', '4'); el.attr('rows', '4');
} }
let val = len + ' / ' + limit; let val = len + ' / ' + limit;
if(len > limit) { if(len > limit) {
let diff = len - limit; let diff = len - limit;
val = '<span class="text-danger">-' + diff + '</span> / ' + limit; val = '<span class="text-danger">-' + diff + '</span> / ' + limit;
} }
$('.bio-counter').html(val); $('.bio-counter').html(val);
$('#bio').on('change keyup paste', function(e) { $('#bio').on('change keyup paste', function(e) {
let el = $(this); let el = $(this);
let len = el.val().length; let len = el.val().length;
let limit = el.data('max-length'); let limit = el.data('max-length');
if(len > 100) { if(len > 100) {
el.attr('rows', '4'); el.attr('rows', '4');
} }
let val = len + ' / ' + limit; let val = len + ' / ' + limit;
if(len > limit) { if(len > limit) {
let diff = len - limit; let diff = len - limit;
val = '<span class="text-danger">-' + diff + '</span> / ' + limit; val = '<span class="text-danger">-' + diff + '</span> / ' + limit;
} }
$('.bio-counter').html(val); $('.bio-counter').html(val);
}); });
$(document).on('click', '.modal-close', function(e) { $(document).on('click', '.modal-close', function(e) {
swal.close(); swal.close();
}); });
$('#maxAvatarSize').text(filesize({{config('pixelfed.max_avatar_size') * 1024}}, {round: 0})); $('#maxAvatarSize').text(filesize({{config('pixelfed.max_avatar_size') * 1024}}, {round: 0}));
$('#avatarInput').on('change', function(e) { $('#avatarInput').on('change', function(e) {
var file = document.getElementById('avatarInput').files[0]; var file = document.getElementById('avatarInput').files[0];
var reader = new FileReader(); var reader = new FileReader();
reader.addEventListener("load", function() { reader.addEventListener("load", function() {
$('#previewAvatar').html('<img src="' + reader.result + '" class="rounded-circle box-shadow mb-3" width="100%" height="100%"/>'); $('#previewAvatar').html('<img src="' + reader.result + '" class="rounded-circle box-shadow mb-3" width="100%" height="100%"/>');
}, false); }, false);
if (file) { if (file) {
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
}); });
$('.delete-profile-photo').on('click', function(e) { $('.delete-profile-photo').on('click', function(e) {
e.preventDefault(); e.preventDefault();
if(window.confirm('Are you sure you want to delete your profile photo.') == false) { if(window.confirm('Are you sure you want to delete your profile photo.') == false) {
return; return;
} }
axios.delete('/settings/avatar').then(res => { axios.delete('/settings/avatar').then(res => {
window.location.href = window.location.href; window.location.href = window.location.href;
}).catch(err => { }).catch(err => {
swal('Error', 'An error occured, please try again later', 'error'); swal('Error', 'An error occured, please try again later', 'error');
}); });
}); });
}) })
</script> </script>

View file

@ -32,7 +32,7 @@
<li class="nav-item"> <li class="nav-item">
<hr> <hr>
</li> </li>
@if(config('pixelfed.import.instagram.enabled')) @if(config_cache('pixelfed.import.instagram.enabled'))
<li class="nav-item pl-3 {{request()->is('*import*')?'active':''}}"> <li class="nav-item pl-3 {{request()->is('*import*')?'active':''}}">
<a class="nav-link font-weight-light text-muted" href="{{route('settings.import')}}">Import</a> <a class="nav-link font-weight-light text-muted" href="{{route('settings.import')}}">Import</a>
</li> </li>
@ -41,7 +41,7 @@
<a class="nav-link font-weight-light text-muted" href="{{route('settings.dataexport')}}">Data Export</a> <a class="nav-link font-weight-light text-muted" href="{{route('settings.dataexport')}}">Data Export</a>
</li> </li>
@if(config('pixelfed.oauth_enabled') == true) @if(config_cache('pixelfed.oauth_enabled') == true)
<li class="nav-item"> <li class="nav-item">
<hr> <hr>
</li> </li>

View file

@ -20,14 +20,14 @@
<li>Go to <a href="{{config('app.url')}}">{{config('app.url')}}</a>.</li> <li>Go to <a href="{{config('app.url')}}">{{config('app.url')}}</a>.</li>
<li>Click on the register link at the top of the page.</li> <li>Click on the register link at the top of the page.</li>
<li>Enter your name, email address, username and password.</li> <li>Enter your name, email address, username and password.</li>
@if(config('pixelfed.enforce_email_verification') != true) @if(config_cache('pixelfed.enforce_email_verification') != true)
<li>Wait for an account verification email, it may take a few minutes.</li> <li>Wait for an account verification email, it may take a few minutes.</li>
@endif @endif
</ol> </ol>
</div> </div>
</div> </div>
</p> </p>
<p> <p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse2" role="button" aria-expanded="false" aria-controls="collapse2"> <a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse2" role="button" aria-expanded="false" aria-controls="collapse2">
<i class="fas fa-chevron-down mr-2"></i> <i class="fas fa-chevron-down mr-2"></i>
How to I update profile info like name, bio, email? How to I update profile info like name, bio, email?
@ -38,7 +38,7 @@
</div> </div>
</div> </div>
</p> </p>
<p> <p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse3" role="button" aria-expanded="false" aria-controls="collapse3"> <a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse3" role="button" aria-expanded="false" aria-controls="collapse3">
<i class="fas fa-chevron-down mr-2"></i> <i class="fas fa-chevron-down mr-2"></i>
What can I do if a username I want is taken but seems inactive? What can I do if a username I want is taken but seems inactive?
@ -49,18 +49,18 @@
</div> </div>
</div> </div>
</p> </p>
<p> <p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse4" role="button" aria-expanded="false" aria-controls="collapse4"> <a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse4" role="button" aria-expanded="false" aria-controls="collapse4">
<i class="fas fa-chevron-down mr-2"></i> <i class="fas fa-chevron-down mr-2"></i>
Why can't I change my username? Why can't I change my username?
</a> </a>
<div class="collapse" id="collapse4"> <div class="collapse" id="collapse4">
<div class="mt-2"> <div class="mt-2">
Pixelfed is a federated application, changing your username is not supported in every <a href="https://en.wikipedia.org/wiki/ActivityPub">federated software</a> so we cannot allow username changes. Your best option is to create a new account with your desired username. Pixelfed is a federated application, changing your username is not supported in every <a href="https://en.wikipedia.org/wiki/ActivityPub">federated software</a> so we cannot allow username changes. Your best option is to create a new account with your desired username.
</div> </div>
</div> </div>
</p> </p>
<p> <p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse5" role="button" aria-expanded="false" aria-controls="collapse5"> <a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse5" role="button" aria-expanded="false" aria-controls="collapse5">
<i class="fas fa-chevron-down mr-2"></i> <i class="fas fa-chevron-down mr-2"></i>
I received an email that I created an account, but I never signed up for one. I received an email that I created an account, but I never signed up for one.
@ -71,7 +71,7 @@
</div> </div>
</div> </div>
</p> </p>
<p> <p>
<a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse6" role="button" aria-expanded="false" aria-controls="collapse6"> <a class="text-dark font-weight-bold" data-toggle="collapse" href="#collapse6" role="button" aria-expanded="false" aria-controls="collapse6">
<i class="fas fa-chevron-down mr-2"></i> <i class="fas fa-chevron-down mr-2"></i>
I can't create a new account because an account with this email already exists. I can't create a new account because an account with this email already exists.
@ -83,4 +83,4 @@
</div> </div>
</p> </p>
@endsection @endsection

View file

@ -1,4 +1,4 @@
@extends('layouts.anon',['title' => 'About ' . config('app.name')]) @extends('layouts.anon',['title' => 'About ' . config_cache('app.name')])
@section('content') @section('content')