Merge pull request #15027 from snipe/feature/sc-26112/allow_default_avatar

Added #15015 - ability for admins to select default avatar
This commit is contained in:
snipe 2024-07-04 17:10:10 +01:00 committed by GitHub
commit edd61705dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 358 additions and 62 deletions

View file

@ -348,7 +348,6 @@ class SettingsController extends Controller
} }
$setting->default_eula_text = $request->input('default_eula_text'); $setting->default_eula_text = $request->input('default_eula_text');
$setting->load_remote = $request->input('load_remote', 0);
$setting->thumbnail_max_h = $request->input('thumbnail_max_h'); $setting->thumbnail_max_h = $request->input('thumbnail_max_h');
$setting->privacy_policy_link = $request->input('privacy_policy_link'); $setting->privacy_policy_link = $request->input('privacy_policy_link');
$setting->depreciation_method = $request->input('depreciation_method'); $setting->depreciation_method = $request->input('depreciation_method');
@ -393,10 +392,11 @@ class SettingsController extends Controller
* *
* @since [v1.0] * @since [v1.0]
* *
* @return View * @return \Illuminate\Contracts\View\View | \Illuminate\Http\RedirectResponse
*/ */
public function postBranding(ImageUploadRequest $request) public function postBranding(ImageUploadRequest $request)
{ {
// Something has gone horribly wrong - no settings record exists!
if (is_null($setting = Setting::getSettings())) { if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error')); return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
} }
@ -407,51 +407,75 @@ class SettingsController extends Controller
$setting->version_footer = $request->input('version_footer'); $setting->version_footer = $request->input('version_footer');
$setting->footer_text = $request->input('footer_text'); $setting->footer_text = $request->input('footer_text');
$setting->skin = $request->input('skin'); $setting->skin = $request->input('skin');
$setting->allow_user_skin = $request->input('allow_user_skin'); $setting->allow_user_skin = $request->input('allow_user_skin', '0');
$setting->show_url_in_emails = $request->input('show_url_in_emails', '0'); $setting->show_url_in_emails = $request->input('show_url_in_emails', '0');
$setting->logo_print_assets = $request->input('logo_print_assets', '0'); $setting->logo_print_assets = $request->input('logo_print_assets', '0');
$setting->load_remote = $request->input('load_remote', 0);
// Only allow the site name and CSS to be changed if lock_passwords is false // Only allow the site name, images, and CSS to be changed if lock_passwords is false
// Because public demos make people act like dicks // Because public demos make people act like dicks
if (!config('app.lock_passwords')) { if (!config('app.lock_passwords')) {
if ($request->has('site_name')) {
$request->validate(['site_name' => 'required']); $request->validate(['site_name' => 'required']);
$setting->site_name = $request->input('site_name'); }
$setting->site_name = $request->input('site_name', 'Snipe-IT');
$setting->custom_css = $request->input('custom_css'); $setting->custom_css = $request->input('custom_css');
// Logo upload
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo'); $setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
if ('1' == $request->input('clear_logo')) { if ($request->input('clear_logo') == '1') {
if (($setting->logo) && (Storage::exists($setting->logo))) {
Storage::disk('public')->delete($setting->logo); Storage::disk('public')->delete($setting->logo);
}
$setting->logo = null; $setting->logo = null;
$setting->brand = 1; $setting->brand = 1;
} }
// Email logo upload
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo'); $setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if ($request->input('clear_email_logo') == '1') {
if (($setting->email_logo) && (Storage::exists($setting->email_logo))) {
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo); Storage::disk('public')->delete($setting->email_logo);
}
$setting->email_logo = null; $setting->email_logo = null;
// If they are uploading an image, validate it and upload it // If they are uploading an image, validate it and upload it
} }
// Label logo upload
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo'); $setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ($request->input('clear_label_logo') == '1') {
if ('1' == $request->input('clear_label_logo')) { if (($setting->label_logo) && (Storage::exists($setting->label_logo))) {
Storage::disk('public')->delete($setting->label_logo); Storage::disk('public')->delete($setting->label_logo);
}
$setting->label_logo = null; $setting->label_logo = null;
} }
// Favicon upload
$setting = $request->handleImages($setting, 600, 'favicon', '', 'favicon'); $setting = $request->handleImages($setting, 100, 'favicon', '', 'favicon');
// If the user wants to clear the favicon...
if ('1' == $request->input('clear_favicon')) { if ('1' == $request->input('clear_favicon')) {
if (($setting->favicon) && (Storage::exists($setting->favicon))) {
Storage::disk('public')->delete($setting->favicon); Storage::disk('public')->delete($setting->favicon);
}
$setting->favicon = null; $setting->favicon = null;
} }
// Default avatar upload
$setting = $request->handleImages($setting, 500, 'default_avatar', 'avatars', 'default_avatar');
if ($request->input('clear_default_avatar') == '1') {
if (($setting->default_avatar) && (Storage::exists('avatars/'.$setting->default_avatar))) {
Storage::disk('public')->delete('avatars/'.$setting->default_avatar);
}
$setting->default_avatar = null;
}
} }
if ($setting->save()) { if ($setting->save()) {

View file

@ -97,9 +97,6 @@ class ImageUploadRequest extends Request
$ext = $image->guessExtension(); $ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext; $file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
Log::info('File name will be: '.$file_name);
Log::debug('File extension is: '.$ext);
if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) { if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
// If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing // If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
// icons or avif, and webp support and needs to be compiled into gd for resizing to be available // icons or avif, and webp support and needs to be compiled into gd for resizing to be available

View file

@ -24,7 +24,7 @@ class UsersTransformer
$array = [ $array = [
'id' => (int) $user->id, 'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar), 'avatar' => e($user->present()->gravatar) ?? null,
'name' => e($user->getFullNameAttribute()), 'name' => e($user->getFullNameAttribute()),
'first_name' => e($user->first_name), 'first_name' => e($user->first_name),
'last_name' => e($user->last_name), 'last_name' => e($user->last_name),

View file

@ -432,6 +432,8 @@ class UserPresenter extends Presenter
*/ */
public function gravatar() public function gravatar()
{ {
// User's specific avatar
if ($this->avatar) { if ($this->avatar) {
// Check if it's a google avatar or some external avatar // Check if it's a google avatar or some external avatar
@ -443,6 +445,12 @@ class UserPresenter extends Presenter
return Storage::disk('public')->url('avatars/'.e($this->avatar)); return Storage::disk('public')->url('avatars/'.e($this->avatar));
} }
// If there is a default avatar
if (Setting::getSettings()->default_avatar!= '') {
return Storage::disk('public')->url('avatars/'.e(Setting::getSettings()->default_avatar));
}
// Fall back to Gravatar if the settings allow loading remote scripts
if (Setting::getSettings()->load_remote == '1') { if (Setting::getSettings()->load_remote == '1') {
if ($this->model->gravatar != '') { if ($this->model->gravatar != '') {
@ -456,8 +464,8 @@ class UserPresenter extends Presenter
} }
} }
// Set a fun, gender-neutral default icon
return config('app.url').'/img/default-sm.png'; return false;
} }
/** /**

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->string('default_avatar')->after('favicon')->default('default.png')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('default_avatar');
});
}
};

View file

@ -122,8 +122,8 @@ return [
'ldap_test' => 'Test LDAP', 'ldap_test' => 'Test LDAP',
'ldap_test_sync' => 'Test LDAP Synchronization', 'ldap_test_sync' => 'Test LDAP Synchronization',
'license' => 'Software License', 'license' => 'Software License',
'load_remote' => 'Use Gravatar', 'load_remote' => 'Load Remote Avatars',
'load_remote_help_text' => 'Uncheck this box if your install cannot load scripts from the outside internet. This will prevent Snipe-IT from trying load images from Gravatar.', 'load_remote_help_text' => 'Uncheck this box if your install cannot load scripts from the outside internet. This will prevent Snipe-IT from trying load avatars from Gravatar or other outside sources.',
'login' => 'Login Attempts', 'login' => 'Login Attempts',
'login_attempt' => 'Login Attempt', 'login_attempt' => 'Login Attempt',
'login_ip' => 'IP Address', 'login_ip' => 'IP Address',
@ -375,5 +375,6 @@ return [
'timezone' => 'Timezone', 'timezone' => 'Timezone',
'profile_edit' => 'Edit Profile', 'profile_edit' => 'Edit Profile',
'profile_edit_help' => 'Allow users to edit their own profiles.', 'profile_edit_help' => 'Allow users to edit their own profiles.',
'default_avatar' => 'Upload default avatar',
]; ];

View file

@ -324,7 +324,7 @@ dir="{{ in_array(app()->getLocale(),['ar-SA','fa-IR', 'he-IL']) ? 'rtl' : 'ltr'
<img src="{{ Auth::user()->present()->gravatar() }}" class="user-image" <img src="{{ Auth::user()->present()->gravatar() }}" class="user-image"
alt=""> alt="">
@else @else
<i class="fas fa-users" aria-hidden="true"></i> <i class="fas fa-user" aria-hidden="true"></i>
@endif @endif
<span class="hidden-xs">{{ Auth::user()->getFullNameAttribute() }} <strong <span class="hidden-xs">{{ Auth::user()->getFullNameAttribute() }} <strong

View file

@ -9,7 +9,7 @@
<div class="col-md-9"> <div class="col-md-9">
<label class="btn btn-default{{ (config('app.lock_passwords')) ? ' disabled' : '' }}"> <label class="btn btn-default{{ (config('app.lock_passwords')) ? ' disabled' : '' }}">
{{ trans('button.select_file') }} {{ trans('button.select_file') }}
<input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="{{ (isset($allowedTypes) ? $allowedTypes : "image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml") }}" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}" <input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="{{ $allowedTypes ?? "image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml" }}" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}"
style="display:none; max-width: 90%"{{ (config('app.lock_passwords')) ? ' disabled' : '' }}> style="display:none; max-width: 90%"{{ (config('app.lock_passwords')) ? ' disabled' : '' }}>
</label> </label>
@ -28,11 +28,10 @@
</div> </div>
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
@if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(($logoPath ?? ''). $snipeSettings->$logoVariable)))
@if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(e($snipeSettings->$logoVariable))))
<div class="pull-left" style="padding-right: 20px;"> <div class="pull-left" style="padding-right: 20px;">
<a href="{{ Storage::disk('public')->url(e($snipeSettings->$logoVariable)) }}"{!! ($logoVariable!='favicon') ? ' data-toggle="lightbox"' : '' !!}> <a href="{{ Storage::disk('public')->url(e(($logoPath ?? '').$snipeSettings->$logoVariable)) }}"{!! ($logoVariable!='favicon') ? ' data-toggle="lightbox"' : '' !!}>
<img id="{{ $logoId }}-imagePreview" style="height: 80px; padding-bottom: 5px;" alt="" src="{{ Storage::disk('public')->url(e($snipeSettings->$logoVariable)) }}"> <img id="{{ $logoId }}-imagePreview" style="height: 80px; padding-bottom: 5px;" alt="" src="{{ Storage::disk('public')->url(e(($logoPath ?? ''). $snipeSettings->$logoVariable)) }}">
</a> </a>
</div> </div>
@endif @endif
@ -44,7 +43,7 @@
</div> </div>
@if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(e($snipeSettings->$logoVariable)))) @if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(($logoPath ?? '').$snipeSettings->$logoVariable)))
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<label id="{{ $logoId }}-deleteCheckbox" for="{{ $logoClearVariable }}" style="font-weight: normal" class="form-control"> <label id="{{ $logoId }}-deleteCheckbox" for="{{ $logoClearVariable }}" style="font-weight: normal" class="form-control">

View file

@ -109,6 +109,36 @@
"maxSize" => 20000 "maxSize" => 20000
]) ])
<!-- Default Avatar -->
@include('partials/forms/edit/uploadLogo', [
"logoVariable" => "default_avatar",
"logoId" => "defaultAvatar",
"logoLabel" => trans('admin/settings/general.default_avatar'),
"logoClearVariable" => "clear_default_avatar",
"logoPath" => "avatars/",
"helpBlock" => trans('general.image_filetypes_help', ['size' => Helper::file_upload_max_size_readable()]),
])
<!-- Load gravatar -->
<div class="form-group {{ $errors->has('load_remote') ? 'error' : '' }}">
<div class="col-md-3">
<strong>{{ trans('admin/settings/general.load_remote') }}</strong>
</div>
<div class="col-md-9">
<label class="form-control">
{{ Form::checkbox('load_remote', '1', old('load_remote', $setting->load_remote)) }}
{{ trans('general.yes') }}
{!! $errors->first('load_remote', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</label>
<p class="help-block">
{{ trans('admin/settings/general.load_remote_help_text') }}
</p>
</div>
</div>
<!-- Include logo in print assets --> <!-- Include logo in print assets -->
<div class="form-group"> <div class="form-group">
<div class="col-md-3"> <div class="col-md-3">

View file

@ -144,24 +144,6 @@
</div> </div>
</div> </div>
<!-- Load gravatar -->
<div class="form-group {{ $errors->has('load_remote') ? 'error' : '' }}">
<div class="col-md-3">
<strong>{{ trans('admin/settings/general.load_remote') }}</strong>
</div>
<div class="col-md-9">
<label class="form-control">
{{ Form::checkbox('load_remote', '1', old('load_remote', $setting->load_remote)) }}
{{ trans('general.yes') }}
{!! $errors->first('load_remote', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</label>
<p class="help-block">
{{ trans('admin/settings/general.load_remote_help_text') }}
</p>
</div>
</div>
<!-- unique serial --> <!-- unique serial -->
<div class="form-group"> <div class="form-group">

View file

@ -2,15 +2,242 @@
namespace Tests\Feature\Settings; namespace Tests\Feature\Settings;
use App\Models\User;
use Tests\TestCase; use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use App\Models\User;
use App\Models\Setting;
class BrandingSettingsTest extends TestCase class BrandingSettingsTest extends TestCase
{ {
public function testSiteNameIsRequired() public function testSiteNameIsRequired()
{ {
$this->actingAs(User::factory()->superuser()->create()) $response = $this->actingAs(User::factory()->superuser()->create())
->from(route('settings.branding.index'))
->post(route('settings.branding.save', ['site_name' => ''])) ->post(route('settings.branding.save', ['site_name' => '']))
->assertInvalid('site_name'); ->assertSessionHasErrors(['site_name'])
->assertInvalid(['site_name'])
->assertStatus(302)
->assertRedirect(route('settings.branding.index'));
$this->followRedirects($response)->assertSee(trans('general.error'));
} }
public function testSiteNameCanBeSaved()
{
$response = $this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save', ['site_name' => 'My Awesome Site']))
->assertStatus(302)
->assertValid('site_name')
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$this->followRedirects($response)->assertSee('Success');
}
public function testLogoCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('public');
$this->actingAs(User::factory()->superuser()->create())
->post(
route('settings.branding.save',
['logo' => UploadedFile::fake()->image('logo.jpg')])
)->assertValid('logo')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::first();
$this->assertNotNull($setting->logo);
$this->assertDatabaseHas('settings', ['logo' => $setting->logo]);
Storage::disk('public')->assertExists($setting->logo);
}
public function testLogoCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['logo' => UploadedFile::fake()->image('logo.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_logo' => '1']));
Storage::disk('testdisk')->assertMissing('logo.jpg');
$setting->refresh();
$this->assertNull($setting->logo);
}
public function testEmailLogoCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['email_logo' => UploadedFile::fake()->image('email-logo.jpg')]
))
->assertValid('email_logo')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::getSettings()->first();
\Log::error($setting->toArray());
Storage::disk('testdisk')->assertExists($setting->email_logo);
}
public function testEmailLogoCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['email_logo' => UploadedFile::fake()->image('email-logo.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_email_logo' => '1']));
Storage::disk('testdisk')->assertMissing('email-logo.jpg');
$setting->refresh();
$this->assertNull($setting->email_logo);
}
public function testLabelLogoCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['label_logo' => UploadedFile::fake()->image('label-logo.jpg')]
))
->assertValid('label_logo')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::getSettings()->first();
Storage::disk('testdisk')->assertExists($setting->label_logo);
}
public function testLabelLogoCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['label_logo' => UploadedFile::fake()->image('label-logo.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_label_logo' => '1']));
Storage::disk('testdisk')->assertMissing('label-logo.jpg');
$setting->refresh();
$this->assertNull($setting->label_logo);
}
public function testDefaultAvatarCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
$setting = Setting::getSettings()->first();
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['default_avatar' => UploadedFile::fake()->image('default-avatar.jpg')]
))
->assertValid('default_avatar')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting->refresh();
Storage::disk('testdisk')->assertExists($setting->default_avatar);
}
public function testDefaultAvatarCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['default_avatar' => UploadedFile::fake()->image('default-avatar.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_default_avatar' => '1']));
Storage::disk('testdisk')->assertMissing('default-avatar.jpg');
$setting->refresh();
$this->assertNull($setting->default_avatar);
}
public function testFaviconCanBeUploaded()
{
$this->markTestIncomplete('This fails mimetype validation on the mock');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['favicon' => UploadedFile::fake()->image('favicon.svg')]
))
->assertValid('favicon')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::getSettings()->first();
Storage::disk('testdisk')->assertExists($setting->favicon);
}
public function testFaviconCanBeDeleted()
{
$this->markTestIncomplete('This fails mimetype validation on the mock');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['favicon' => UploadedFile::fake()->image('favicon.ico')->mimeType('image/x-icon')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_favicon' => '1']));
Storage::disk('testdisk')->assertMissing('favicon.ico');
$setting->refresh();
$this->assertNull($setting->favicon);
}
} }