Updates for company import'

This commit is contained in:
David Bomba
2025-12-29 10:21:16 +11:00
5 changed files with 185 additions and 11 deletions

View File

@@ -72,6 +72,68 @@ class StoreSubscriptionRequest extends Request
return $this->globalRules($rules);
}
/**
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator(\Illuminate\Validation\Validator $validator): void
{
$validator->after(function ($validator) {
$this->validateWebhookUrl($validator, 'webhook_configuration.post_purchase_url');
});
}
/**
* Validate that a URL doesn't point to internal/private IP addresses.
*
* @param \Illuminate\Validation\Validator $validator
* @param string $field
* @return void
*/
private function validateWebhookUrl(\Illuminate\Validation\Validator $validator, string $field): void
{
$url = $this->input($field);
if (empty($url)) {
return;
}
// Validate URL format
if (!filter_var($url, FILTER_VALIDATE_URL)) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
return;
}
$parsed = parse_url($url);
// Only allow http/https protocols
$scheme = $parsed['scheme'] ?? '';
if (!in_array(strtolower($scheme), ['http', 'https'])) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
return;
}
$host = $parsed['host'] ?? '';
if (empty($host)) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
return;
}
// Resolve hostname to IP and check for private/reserved ranges
$ip = gethostbyname($host);
// gethostbyname returns the hostname if resolution fails
if ($ip === $host && !filter_var($host, FILTER_VALIDATE_IP)) {
// DNS resolution failed - allow it (external DNS might resolve differently)
return;
}
// Block private and reserved IP ranges (SSRF protection)
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
}
}
public function prepareForValidation()
{
$input = $this->all();

View File

@@ -73,6 +73,69 @@ class UpdateSubscriptionRequest extends Request
return $this->globalRules($rules);
}
/**
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator(\Illuminate\Validation\Validator $validator): void
{
$validator->after(function ($validator) {
$this->validateWebhookUrl($validator, 'webhook_configuration.post_purchase_url');
});
}
/**
* Validate that a URL doesn't point to internal/private IP addresses.
*
* @param \Illuminate\Validation\Validator $validator
* @param string $field
* @return void
*/
private function validateWebhookUrl(\Illuminate\Validation\Validator $validator, string $field): void
{
$url = $this->input($field);
if (empty($url)) {
return;
}
// Validate URL format
if (!filter_var($url, FILTER_VALIDATE_URL)) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
return;
}
$parsed = parse_url($url);
// Only allow http/https protocols
$scheme = $parsed['scheme'] ?? '';
if (!in_array(strtolower($scheme), ['http', 'https'])) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
return;
}
$host = $parsed['host'] ?? '';
if (empty($host)) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
return;
}
// Resolve hostname to IP and check for private/reserved ranges
$ip = gethostbyname($host);
// gethostbyname returns the hostname if resolution fails
if ($ip === $host && !filter_var($host, FILTER_VALIDATE_IP)) {
// DNS resolution failed - allow it (external DNS might resolve differently)
return;
}
// Block private and reserved IP ranges (SSRF protection)
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
$validator->errors()->add($field, ctrans('texts.invalid_url'));
}
}
public function prepareForValidation()
{
$input = $this->all();

View File

@@ -472,16 +472,56 @@ class Import implements ShouldQueue
$company_repository->save($data, $this->company);
if (isset($data['settings']->company_logo) && strlen($data['settings']->company_logo) > 0) {
try {
$tempImage = tempnam(sys_get_temp_dir(), basename($data['settings']->company_logo));
copy($data['settings']->company_logo, $tempImage);
$this->uploadLogo($tempImage, $this->company, $this->company);
$logoUrl = $data['settings']->company_logo;
// 1. Validate URL format
if (!filter_var($logoUrl, FILTER_VALIDATE_URL)) {
throw new \Exception('Invalid URL format');
}
// 2. Restrict protocols
$parsed = parse_url($logoUrl);
if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
throw new \Exception('Only HTTP/HTTPS allowed');
}
// 3. Block internal/private IPs (SSRF protection)
$host = $parsed['host'] ?? '';
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
throw new \Exception('Internal hosts not allowed');
}
// 4. Use HTTP client with timeout and size limits instead of copy()
$response = \Illuminate\Support\Facades\Http::timeout(20)->get($logoUrl);
if ($response->successful() && strlen($response->body()) < 20 * 1024 * 1024) { // 5MB limit
$tempImage = tempnam(sys_get_temp_dir(), 'logo_');
file_put_contents($tempImage, $response->body());
$this->uploadLogo($tempImage, $this->company, $this->company);
@unlink($tempImage); // Cleanup
}
} catch (\Exception $e) {
$settings = $this->company->settings;
$settings->company_logo = '';
$this->company->settings = $settings;
$this->company->save();
nlog("Logo import failed: " . $e->getMessage());
}
// try {
// $tempImage = tempnam(sys_get_temp_dir(), basename($data['settings']->company_logo));
// copy($data['settings']->company_logo, $tempImage);
// $this->uploadLogo($tempImage, $this->company, $this->company);
// } catch (\Exception $e) {
// $settings = $this->company->settings;
// $settings->company_logo = '';
// $this->company->settings = $settings;
// $this->company->save();
// }
}
Company::reguard();

View File

@@ -701,7 +701,7 @@ class StripePaymentDriver extends BaseDriver implements SupportsHeadlessInterfac
public function processWebhookRequest(PaymentWebhookRequest $request)
{
nlog($request->all());
// nlog($request->all());
$webhook_secret = $this->company_gateway->getConfigField('webhookSecret');
if ($webhook_secret) {

View File

@@ -12,11 +12,11 @@
namespace App\Services\EDocument\Standards;
use App\DataMapper\Tax\BaseRule;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
use App\Helpers\Invoice\Taxer;
use App\DataMapper\Tax\BaseRule;
use App\Services\AbstractService;
use App\Helpers\Invoice\InvoiceSum;
use InvoiceNinja\EInvoice\EInvoice;
@@ -654,9 +654,11 @@ class Peppol extends AbstractService
case Product::PRODUCT_TYPE_DIGITAL:
case Product::PRODUCT_TYPE_PHYSICAL:
case Product::PRODUCT_TYPE_SHIPPING:
case Product::PRODUCT_TYPE_REDUCED_TAX:
$tax_type = 'S';
break;
case Product::PRODUCT_TYPE_REDUCED_TAX:
$tax_type = 'AA';
break;
case Product::PRODUCT_TYPE_EXEMPT:
$tax_type = 'E';
break;
@@ -1405,7 +1407,7 @@ class Peppol extends AbstractService
$tax_total = new TaxTotal();
$taxes = $this->calc->getTaxMap();
if (count($taxes) < 1) {
if (count($taxes) < 1 || (count($taxes) == 1 && $this->invoice->total_taxes == 0)) {
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
@@ -1458,7 +1460,8 @@ class Peppol extends AbstractService
// Required: TaxAmount (BT-110)
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = (string)$grouped_tax['total'];
// $tax_amount->amount = (string)$grouped_tax['total'];
$tax_amount->amount = (string)round($this->invoice->total_taxes, 2);
$tax_total->TaxAmount = $tax_amount;
// Required: TaxSubtotal (BG-23)
@@ -1490,6 +1493,11 @@ class Peppol extends AbstractService
$category_id = new ID();
$category_id->value = $this->getTaxType($grouped_tax['tax_id']); // Standard rate
// Temp fix for reduced tax rate categorization.
if($grouped_tax['tax_rate'] < 15 && $grouped_tax['tax_rate'] >= 0) {
$category_id->value = 'AA';
}
$tax_category->ID = $category_id;
// Required: TaxCategory Rate (BT-119)
@@ -1504,7 +1512,8 @@ class Peppol extends AbstractService
$tax_scheme->ID = $scheme_id;
$tax_category->TaxScheme = $tax_scheme;
$tax_subtotal->TaxCategory = $this->globalTaxCategories[0];
$tax_subtotal->TaxCategory = $tax_category;
// $tax_subtotal->TaxCategory = $this->globalTaxCategories[0];
$tax_total->TaxSubtotal[] = $tax_subtotal;
@@ -1533,8 +1542,8 @@ class Peppol extends AbstractService
$country_code = $this->invoice->client->country->iso_3166_2;
if (isset($this->ninja_invoice->company->tax_data->regions->EU->subregions->{$country_code}->vat_number)) {
$this->override_vat_number = $this->ninja_invoice->company->tax_data->regions->EU->subregions->{$country_code}->vat_number;
if (isset($this->company->tax_data->regions->EU->subregions->{$country_code}->vat_number)) {
$this->override_vat_number = $this->company->tax_data->regions->EU->subregions->{$country_code}->vat_number;
}
}
}