attributes['purchase_date'] = $value; } /** * @return Attribute */ protected function purchaseDateFormatted(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes) => $attributes['purchase_date'] ? Helper::getFormattedDateObject(Carbon::parse($attributes['purchase_date']), 'date', false) : null, ); } /** * @return Attribute */ protected function expiresDiffInDays(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes) => array_key_exists('expiration_date', $attributes) ? Carbon::now()->diffInDays($attributes['expiration_date']) : null, ); } /** * @return Attribute */ protected function expiresDiffForHumans(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes) => array_key_exists('expiration_date', $attributes) ? Carbon::parse($attributes['expiration_date'])->diffForHumans() : null, ); } /** * @return Attribute */ protected function expiresFormattedDate(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes) => array_key_exists('expiration_date', $attributes) ? Helper::getFormattedDateObject($attributes['expiration_date'], 'date', false) : null, ); } public function setPurchaseCostAttribute($value) { if (is_numeric($value)) { // value is *already* a floating-point number. Just assign it directly $this->attributes['purchase_cost'] = $value; return; } $value = Helper::ParseCurrency($value); if ($value == 0) { $value = null; } $this->attributes['purchase_cost'] = $value; } public function setLocationIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['location_id'] = $value; } public function setCategoryIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['category_id'] = $value; } public function setSupplierIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['supplier_id'] = $value; } public function setDepreciationIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['depreciation_id'] = $value; } public function setManufacturerIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['manufacturer_id'] = $value; } public function setMinAmtAttribute($value) { if ($value == '') { $value = null; } $this->attributes['min_amt'] = $value; } public function setParentIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['parent_id'] = $value; } public function setFieldSetIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['fieldset_id'] = $value; } public function setCompanyIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['company_id'] = $value; } public function setWarrantyMonthsAttribute($value) { if ($value == '') { $value = null; } $this->attributes['warranty_months'] = $value; } public function setRtdLocationIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['rtd_location_id'] = $value; } public function setDepartmentIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['department_id'] = $value; } public function setManagerIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['manager_id'] = $value; } public function setModelIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['model_id'] = $value; } public function setStatusIdAttribute($value) { if ($value == '') { $value = null; } $this->attributes['status_id'] = $value; } /** * Applies offset (from request) and limit to query. * * @return void */ public function scopeApplyOffsetAndLimit(Builder $query, int $total) { $offset = (request()->input('offset') > $total) ? $total : app('api_offset_value'); $limit = app('api_limit_value'); $query->skip($offset)->take($limit); } /** * @return Attribute */ protected function displayName(): Attribute { return Attribute::make( get: fn (mixed $value) => $this->name, ); } public function getEula() { // Resolve the raw eula text from the appropriate source, then hand // it to sanitizeEulaForRender before returning. See that method for // the security rationale behind the sanitize step. $raw = null; // This is - for now - only for assets, where the asset model is the thing tied to the category if (($this->model) && ($this->model->category)) { if (($this->model->category->eula_text) && ($this->model->category->use_default_eula == 0)) { $raw = $this->model->category->eula_text; } elseif ($this->model->category->use_default_eula == 1) { $raw = Setting::getSettings()->default_eula_text; } else { return false; } // For everything else, just check the category for EULA info } elseif (($this->category) && ($this->category->eula_text)) { $raw = $this->category->eula_text; } elseif ((Setting::getSettings()->default_eula_text) && (($this->category) && ($this->category->use_default_eula == '1'))) { $raw = Setting::getSettings()->default_eula_text; } return $this->sanitizeEulaForRender($raw); } /** * Sanitize raw eula_text before it lands in any renderer. This method * is invoked by getEula and mirrors the shape Category::getEula uses on * the web path (Helper::parseEscapedMarkedown = strip_tags + Parsedown * safe mode) with one addition: an strip on the Parsedown output. * * The extra strip is what closes the LFR + SSRF primitive reported * by W1nterFr3ak (Chris Byron Otieno) on 2026-08-02. Every checkout mail * template embeds this via `{!! $eula !!}` into a Markdown mailable, * whose HTML output is walked by laravel-mail-auto-embed, which fetches * every server-side (file_get_contents for local paths, * curl with TLS verification disabled for remote URLs) and attaches the * bytes to the outgoing mail. Any low-privilege user with categories.edit * could set eula_text to `![x](/var/www/html/.env)` or a raw tag, * check the asset out to themselves, and receive the file contents (or * the response body of any URL, including cloud instance metadata) as * a MIME attachment. * * strip_tags kills raw HTML the user might have typed directly. * Parsedown safe mode converts markdown to HTML. The second img-strip * removes markdown-syntax images that Parsedown converted * (e.g. `![x](url)` becoming ``). BlockImagesMarkdownExtension * on the mail Markdown parser (see config/mail.php) is defense in depth * for anything that slips past this pre-sanitize. */ protected function sanitizeEulaForRender(?string $raw): ?string { if ($raw === null || $raw === '') { return null; } $rendered = Helper::parseEscapedMarkedown($raw); if ($rendered === null || $rendered === '') { return null; } return preg_replace('/]*>/i', '', $rendered); } public function getImageUrl($path = null) { // If there is a consumable image, use that if ($this->image) { return Storage::disk('public')->url($path.$this->image); } return false; } public function actionlog() { return $this->hasMany(Actionlog::class, 'target_id')->where('target_type', '=', self::class); } /** * Establishes the object -> admin user relationship * * @return Relation * * @since [v3.0] * * @author [A. Gianotto] [] */ public function adminuser() { return $this->belongsTo(User::class, 'created_by')->withTrashed(); } public function showCheckoutButton($item) { if (method_exists($item, 'numRemaining')) { if ($item->numRemaining() > 0) { return 'show-active'; } return 'show-disabled'; } if (method_exists($item, 'availableForCheckout')) { if ($item->availableForCheckout()) { return 'show-active'; } return 'show-disabled'; } return false; } public function showCheckinButton($item) { if (method_exists($item, 'numRemaining')) { if ($item->numRemaining() <= 0) { return 'show-active'; } return 'show-disabled'; } if (method_exists($item, 'availableForCheckout')) { if ($item->availableForCheckIn()) { return 'show-active'; } return 'show-disabled'; } return false; } }