user())->pending()->get(); return view('account/accept.index', compact('acceptances')); } /** * Shows a form to either accept or decline the checkout acceptance * * @param int $id */ public function create(Request $request, $id): View|RedirectResponse { $currentUser = auth()->user(); if (! $currentUser instanceof User) { abort(403, trans('general.insufficient_permissions')); } $acceptance = CheckoutAcceptance::find($id); if (! $acceptance) { return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist')); } if (! $acceptance->isPending()) { if ($this->isStaleSignInPlaceAdminAttempt($acceptance, $currentUser)) { return $this->redirectToIntendedSignInPlaceDestination($request, $acceptance) ->with('warning', trans('admin/users/message.error.asset_already_accepted')); } return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.asset_already_accepted')); } $isSignInPlaceAdminFlow = $this->isSignInPlaceAdminFlow($acceptance); if (! $acceptance->isCheckedOutTo($currentUser) && (! $isSignInPlaceAdminFlow)) { return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.incorrect_user_accepted')); } if (! Company::isCurrentUserHasAccess($acceptance->checkoutable)) { return redirect()->route('account.accept')->with('error', trans('general.error_user_company')); } $checkedOutAt = Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false); $checkedOutBy = $this->resolveCheckoutActorName($acceptance); return view('account/accept.create', compact('acceptance', 'isSignInPlaceAdminFlow', 'checkedOutAt', 'checkedOutBy')); } /** * Stores the accept/decline of the checkout acceptance * * @param int $id */ public function store(Request $request, $id): RedirectResponse { $currentUser = auth()->user(); if (! $currentUser instanceof User) { abort(403, trans('general.insufficient_permissions')); } $acceptance = CheckoutAcceptance::find($id); if (! $acceptance) { return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist')); } $assignedUser = User::find($acceptance->assigned_to_id); $settings = Setting::getSettings(); $requiresSignature = (string) $settings->require_accept_signature === '1'; $sig_filename = ''; $encodedSignatureImage = null; if (! $acceptance->isPending()) { if ($this->isStaleSignInPlaceAdminAttempt($acceptance, $currentUser)) { return $this->redirectToIntendedSignInPlaceDestination($request, $acceptance) ->with('warning', trans('admin/users/message.error.asset_already_accepted')); } return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.asset_already_accepted')); } $isSignInPlaceAdminFlow = $this->isSignInPlaceAdminFlow($acceptance); if (! $acceptance->isCheckedOutTo($currentUser) && (! $isSignInPlaceAdminFlow)) { return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.incorrect_user_accepted')); } if (! Company::isCurrentUserHasAccess($acceptance->checkoutable)) { return redirect()->route('account.accept')->with('error', trans('general.insufficient_permissions')); } if (! $request->filled('asset_acceptance')) { return redirect()->back()->with('error', trans('admin/users/message.error.accept_or_decline')); } /** * Check for the signature directory */ if (! Storage::exists('private_uploads/signatures')) { Storage::makeDirectory('private_uploads/signatures', 775); } /** * Check for the eula-pdfs directory */ if (! Storage::exists('private_uploads/eula-pdfs')) { Storage::makeDirectory('private_uploads/eula-pdfs', 775); } $item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id); $username_slug = Str::slug($assignedUser->username); $asset_tag_slug = ($item instanceof Asset && $item->asset_tag) ? '-'.Str::slug($item->asset_tag) : ''; // If signatures are required, make sure we have one if ($requiresSignature) { // The item was accepted, check for a signature if ($request->filled('signature_output')) { $sig_filename = 'siglog-'.Str::uuid().'-'.date('Y-m-d-his').'.png'; $dataUri = (string) $request->input('signature_output'); $encodedSignatureImage = Str::contains($dataUri, ',') ? Str::after($dataUri, ',') : $dataUri; $decoded_image = base64_decode($encodedSignatureImage, true); if ($decoded_image === false) { return redirect()->back()->with('error', trans('general.shitty_browser')); } $decoded_image = $this->flattenSignatureBackgroundToWhite($decoded_image); $encodedSignatureImage = base64_encode($decoded_image); Storage::put('private_uploads/signatures/'.$sig_filename, (string) $decoded_image); // No image data is present, kick them back. // This mostly only applies to users on super-duper crapola browsers *cough* IE *cough* } else { return redirect()->back()->with('error', trans('general.shitty_browser')); } } // Convert PDF logo to base64 for TCPDF // This is needed for TCPDF to properly embed the image if it's a png and the cache isn't writable $encoded_logo = null; if (($settings->acceptance_pdf_logo) && (Storage::disk('public')->exists($settings->acceptance_pdf_logo))) { $encoded_logo = base64_encode(file_get_contents(public_path().'/uploads/'.basename($settings->acceptance_pdf_logo))); } // Get the data array ready for the notifications and PDF generation $data = [ 'item_tag' => $item->asset_tag, 'item_name' => $item->display_name, // this handles licenses seats, which don't have a 'name' field 'item_model' => $item->model?->name, 'item_serial' => $item->serial, 'item_status' => $item->status?->name, 'eula' => $item->getEula(), 'note' => $request->input('note'), 'check_out_date' => Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false), 'accepted_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false), 'declined_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false), 'assigned_to' => $assignedUser->display_name, 'email' => $assignedUser->email, 'employee_num' => $assignedUser->employee_num, 'site_name' => $settings->site_name, 'company_name' => $item->company?->name ?? $settings->site_name, 'signature' => ($sig_filename !== '') ? $encodedSignatureImage : null, 'logo' => ($encoded_logo) ?? null, 'date_settings' => $settings->date_display_format, 'qty' => $acceptance->qty ?? 1, ]; // Include asset custom fields that are explicitly allowed in outbound emails/PDFs. if ($item instanceof Asset && $item->model && $item->model->fieldset) { $customFields = []; $fields = $item->model->fieldset->fields ->where('show_in_email', true) ->where('field_encrypted', false); foreach ($fields as $field) { $dbColumn = $field->db_column; $value = $item->{$dbColumn}; if (! is_null($value) && $value !== '') { $customFields[] = [ 'label' => $field->name, 'value' => $value, ]; } } if (! empty($customFields)) { $data['custom_fields'] = $customFields; } } if ($request->input('asset_acceptance') === 'accepted') { $pdf_filename = 'accepted-'.$username_slug.$asset_tag_slug.'-'.date('Y-m-d-h-i-s').'.pdf'; // Generate the PDF content $pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance); Storage::put('private_uploads/eula-pdfs/'.$pdf_filename, $pdf_content); // Log the acceptance $acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note')); // Send the PDF to the signing user if (($request->input('send_copy') === '1') && ($assignedUser->email !== '')) { // Add the attachment for the signing user into the $data array $data['file'] = $pdf_filename; try { $assignedUser->notify((new AcceptanceItemAcceptedToUserNotification($data))->locale($assignedUser->locale)); } catch (Exception $e) { Log::warning($e); } } try { $acceptance->notify((new AcceptanceItemAcceptedNotification($data))->locale(Setting::getSettings()->locale)); } catch (Exception $e) { Log::warning($e); } event(new CheckoutAccepted($acceptance)); $return_msg = trans('admin/users/message.accepted'); // Item was declined } else { for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) { $acceptance->decline($sig_filename, $request->input('note')); } $acceptance->notify(new AcceptanceItemDeclinedNotification($data)); Log::debug('New event acceptance.'); event(new CheckoutDeclined($acceptance)); $return_msg = trans('admin/users/message.declined'); } // Send an email notification if one is requested if ($acceptance->alert_on_response_id) { try { $recipient = User::find($acceptance->alert_on_response_id); if ($recipient?->email) { Log::debug('Attempting to send email acceptance.'); Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail( $acceptance, $recipient, $request->input('asset_acceptance') === 'accepted', )); Log::debug('Send email notification success on checkout acceptance response.'); } } catch (Exception $e) { Log::error($e->getMessage()); Log::warning($e); } } if ($isSignInPlaceAdminFlow) { $request->request->add(['assigned_user' => $assignedUser?->id]); $redirect = Helper::getRedirectOption( $request, session('sign_in_place_item_id'), session('sign_in_place_resource_type'), ); session()->forget([ 'sign_in_place_acceptance_id', 'sign_in_place_item_id', 'sign_in_place_resource_type', ]); return $redirect->with('success', $return_msg); } return redirect()->to('account/accept')->with('success', $return_msg); } private function isSignInPlaceAdminFlow(CheckoutAcceptance $acceptance): bool { $currentUser = auth()->user(); return ((int) session('sign_in_place_acceptance_id') === (int) $acceptance->id) && ($currentUser?->can('checkout', $acceptance->checkoutable)); } private function resolveCheckoutActorName(CheckoutAcceptance $acceptance): ?string { [$itemType, $itemId] = $this->resolveCheckoutLogItem($acceptance); $checkoutLog = Actionlog::query() ->where('action_type', 'checkout') ->where('item_type', $itemType) ->where('item_id', $itemId) ->where('target_type', User::class) ->where('target_id', $acceptance->assigned_to_id) ->where('created_at', '<=', $acceptance->created_at->copy()->addMinutes(5)) ->latest('id') ->first(); return $checkoutLog?->adminuser?->display_name; } /** * Action logs normalize license seat checkouts to the parent license. * * @return array{0: class-string, 1: int} */ private function resolveCheckoutLogItem(CheckoutAcceptance $acceptance): array { $checkoutable = $acceptance->checkoutable; if ($checkoutable instanceof LicenseSeat) { return [License::class, (int) $checkoutable->license_id]; } return [$acceptance->checkoutable_type, (int) $acceptance->checkoutable_id]; } private function isStaleSignInPlaceAdminAttempt(CheckoutAcceptance $acceptance, User $currentUser): bool { $redirectOption = session('redirect_option'); $checkoutToType = session('checkout_to_type'); if (session('sign_in_place') !== true) { return false; } if ($redirectOption === null) { return false; } if ($redirectOption === 'target' && $checkoutToType === 'user' && empty($acceptance->assigned_to_id)) { return false; } return ! $acceptance->isCheckedOutTo($currentUser) && $currentUser->can('checkout', $acceptance->checkoutable) && ($checkoutToType === 'user'); } private function redirectToIntendedSignInPlaceDestination(Request $request, CheckoutAcceptance $acceptance): RedirectResponse { if (empty($acceptance->assigned_to_id)) { return redirect()->route('account.accept'); } [$itemId, $resourceType] = $this->resolveRedirectTarget($acceptance); $request->request->add(['assigned_user' => $acceptance->assigned_to_id]); return Helper::getRedirectOption($request, $itemId, $resourceType); } /** * @return array{0: int, 1: string} */ private function resolveRedirectTarget(CheckoutAcceptance $acceptance): array { $checkoutable = $acceptance->checkoutable; if ($checkoutable instanceof Asset) { return [(int) $checkoutable->id, 'Assets']; } if ($checkoutable instanceof Accessory) { return [(int) $checkoutable->id, 'Accessories']; } if ($checkoutable instanceof Consumable) { return [(int) $checkoutable->id, 'Consumables']; } if ($checkoutable instanceof LicenseSeat) { return [(int) $checkoutable->license_id, 'Licenses']; } return [(int) $acceptance->checkoutable_id, session('sign_in_place_resource_type', 'Assets')]; } private function flattenSignatureBackgroundToWhite(string $signatureBinary): string { if (! function_exists('imagecreatefromstring') || ! function_exists('imagecreatetruecolor')) { return $signatureBinary; } $source = @imagecreatefromstring($signatureBinary); if ($source === false) { return $signatureBinary; } $width = imagesx($source); $height = imagesy($source); $flattened = imagecreatetruecolor($width, $height); if ($flattened === false) { imagedestroy($source); return $signatureBinary; } $white = imagecolorallocate($flattened, 255, 255, 255); imagefilledrectangle($flattened, 0, 0, $width, $height, $white); imagecopy($flattened, $source, 0, 0, 0, 0, $width, $height); ob_start(); imagepng($flattened); $output = ob_get_clean(); imagedestroy($source); imagedestroy($flattened); return is_string($output) ? $output : $signatureBinary; } }