diff --git a/VERSION b/VERSION index 2e1cd7d5ae..ce38df33b3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.7.0-beta +v1.7.1-beta diff --git a/backend/.env.example b/backend/.env.example index 12971f17ac..c5ee655b31 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -13,6 +13,7 @@ APP_HOMEPAGE_VIEWS_UPDATE_BATCH_SIZE=8 APP_DISABLE_REGISTRATION=false APP_STRIPE_CONNECT_ACCOUNT_TYPE=express APP_PLATFORM_SUPPORT_EMAIL=support@your-website.com +APP_ALLOWED_INTERNAL_WEBHOOK_HOSTS= STRIPE_PUBLIC_KEY= STRIPE_SECRET_KEY= diff --git a/backend/app/DomainObjects/ProductCategoryDomainObject.php b/backend/app/DomainObjects/ProductCategoryDomainObject.php index 592d3dcbf5..f1c9ec9143 100644 --- a/backend/app/DomainObjects/ProductCategoryDomainObject.php +++ b/backend/app/DomainObjects/ProductCategoryDomainObject.php @@ -2,10 +2,40 @@ namespace HiEvents\DomainObjects; +use HiEvents\DomainObjects\Interfaces\IsSortable; +use HiEvents\DomainObjects\SortingAndFiltering\AllowedSorts; use Illuminate\Support\Collection; -class ProductCategoryDomainObject extends Generated\ProductCategoryDomainObjectAbstract +class ProductCategoryDomainObject extends Generated\ProductCategoryDomainObjectAbstract implements IsSortable { + public static function getDefaultSort(): string + { + return self::ORDER; + } + + public static function getDefaultSortDirection(): string + { + return 'asc'; + } + + public static function getAllowedSorts(): AllowedSorts + { + return new AllowedSorts([ + self::ORDER => [ + 'asc' => __('Order Ascending'), + 'desc' => __('Order Descending'), + ], + self::CREATED_AT => [ + 'asc' => __('Oldest First'), + 'desc' => __('Newest First'), + ], + self::NAME => [ + 'asc' => __('Name A-Z'), + 'desc' => __('Name Z-A'), + ], + ]); + } + public ?Collection $products = null; public function setProducts(Collection $products): void diff --git a/backend/app/Repository/Eloquent/AffiliateRepository.php b/backend/app/Repository/Eloquent/AffiliateRepository.php index 1fdacd1955..bc89068a18 100644 --- a/backend/app/Repository/Eloquent/AffiliateRepository.php +++ b/backend/app/Repository/Eloquent/AffiliateRepository.php @@ -44,8 +44,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - column: $params->sort_by ?? AffiliateDomainObject::getDefaultSort(), - direction: $params->sort_direction ?? 'desc', + column: $this->validateSortColumn($params->sort_by, AffiliateDomainObject::class), + direction: $this->validateSortDirection($params->sort_direction, AffiliateDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/AttendeeRepository.php b/backend/app/Repository/Eloquent/AttendeeRepository.php index 1fae4a7e5a..8f2ce62ff0 100644 --- a/backend/app/Repository/Eloquent/AttendeeRepository.php +++ b/backend/app/Repository/Eloquent/AttendeeRepository.php @@ -85,8 +85,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware $this->applyFilterFields($params, AttendeeDomainObject::getAllowedFilterFields(), prefix: 'attendees'); } - $sortBy = $params->sort_by ?? AttendeeDomainObject::getDefaultSort(); - $sortDirection = $params->sort_direction ?? AttendeeDomainObject::getDefaultSortDirection(); + $sortBy = $this->validateSortColumn($params->sort_by, AttendeeDomainObject::class); + $sortDirection = $this->validateSortDirection($params->sort_direction, AttendeeDomainObject::class); if ($sortBy === AttendeeDomainObject::TICKET_NAME_SORT_KEY) { $this->model = $this->model diff --git a/backend/app/Repository/Eloquent/BaseRepository.php b/backend/app/Repository/Eloquent/BaseRepository.php index c3d98d1c8d..f00f8717c9 100644 --- a/backend/app/Repository/Eloquent/BaseRepository.php +++ b/backend/app/Repository/Eloquent/BaseRepository.php @@ -7,6 +7,7 @@ use BadMethodCallException; use Carbon\Carbon; use HiEvents\DomainObjects\Interfaces\DomainObjectInterface; +use HiEvents\DomainObjects\Interfaces\IsSortable; use HiEvents\Http\DTO\QueryParamsDTO; use HiEvents\Models\BaseModel; use HiEvents\Repository\Eloquent\Value\Relationship; @@ -54,6 +55,28 @@ public function __construct(Application $application, DatabaseManager $db) */ abstract protected function getModel(): string; + /** + * @param class-string $domainObjectClass + */ + protected function validateSortColumn(?string $sortBy, string $domainObjectClass): string + { + $allowedColumns = array_keys($domainObjectClass::getAllowedSorts()->toArray()); + $default = $domainObjectClass::getDefaultSort(); + + if ($sortBy === null || !in_array($sortBy, $allowedColumns, true)) { + return $default; + } + + return $sortBy; + } + + protected function validateSortDirection(?string $sortDirection, string $domainObjectClass): string + { + return in_array(strtolower($sortDirection ?? ''), ['asc', 'desc'], true) + ? $sortDirection + : $domainObjectClass::getDefaultSortDirection(); + } + public function setMaxPerPage(int $maxPerPage): static { $this->maxPerPage = $maxPerPage; diff --git a/backend/app/Repository/Eloquent/CapacityAssignmentRepository.php b/backend/app/Repository/Eloquent/CapacityAssignmentRepository.php index 5ad6449525..74e82b46aa 100644 --- a/backend/app/Repository/Eloquent/CapacityAssignmentRepository.php +++ b/backend/app/Repository/Eloquent/CapacityAssignmentRepository.php @@ -39,8 +39,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - $params->sort_by ?? CapacityAssignmentDomainObject::getDefaultSort(), - $params->sort_direction ?? CapacityAssignmentDomainObject::getDefaultSortDirection(), + $this->validateSortColumn($params->sort_by, CapacityAssignmentDomainObject::class), + $this->validateSortDirection($params->sort_direction, CapacityAssignmentDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/CheckInListRepository.php b/backend/app/Repository/Eloquent/CheckInListRepository.php index 0e6c62e086..46b37358da 100644 --- a/backend/app/Repository/Eloquent/CheckInListRepository.php +++ b/backend/app/Repository/Eloquent/CheckInListRepository.php @@ -140,8 +140,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - $params->sort_by ?? CheckInListDomainObject::getDefaultSort(), - $params->sort_direction ?? CheckInListDomainObject::getDefaultSortDirection(), + $this->validateSortColumn($params->sort_by, CheckInListDomainObject::class), + $this->validateSortDirection($params->sort_direction, CheckInListDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/EventRepository.php b/backend/app/Repository/Eloquent/EventRepository.php index 824e9d2ccc..51773cc94b 100644 --- a/backend/app/Repository/Eloquent/EventRepository.php +++ b/backend/app/Repository/Eloquent/EventRepository.php @@ -81,8 +81,8 @@ public function findEvents(array $where, QueryParamsDTO $params): LengthAwarePag } $this->model = $this->model->orderBy( - $params->sort_by ?? EventDomainObject::getDefaultSort(), - $params->sort_direction ?? EventDomainObject::getDefaultSortDirection(), + $this->validateSortColumn($params->sort_by, EventDomainObject::class), + $this->validateSortDirection($params->sort_direction, EventDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/MessageRepository.php b/backend/app/Repository/Eloquent/MessageRepository.php index d47bd35ab2..c38216f208 100644 --- a/backend/app/Repository/Eloquent/MessageRepository.php +++ b/backend/app/Repository/Eloquent/MessageRepository.php @@ -46,8 +46,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - $params->sort_by ?? MessageDomainObject::getDefaultSort(), - $params->sort_direction ?? 'desc', + $this->validateSortColumn($params->sort_by, MessageDomainObject::class), + $this->validateSortDirection($params->sort_direction, MessageDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/OrderRepository.php b/backend/app/Repository/Eloquent/OrderRepository.php index 5820db8823..f33c2629ee 100644 --- a/backend/app/Repository/Eloquent/OrderRepository.php +++ b/backend/app/Repository/Eloquent/OrderRepository.php @@ -57,8 +57,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - column: $params->sort_by ?? OrderDomainObject::getDefaultSort(), - direction: $params->sort_direction ?? 'desc', + column: $this->validateSortColumn($params->sort_by, OrderDomainObject::class), + direction: $this->validateSortDirection($params->sort_direction, OrderDomainObject::class), ); return $this->paginateWhere( @@ -102,9 +102,10 @@ public function findByOrganizerId(int $organizerId, int $accountId, QueryParamsD ->where('events.organizer_id', $organizerId) ->where('events.account_id', $accountId); + $sortBy = $this->validateSortColumn($params->sort_by, OrderDomainObject::class); $this->model = $this->model->orderBy( - column: $params->sort_by ? 'orders.' . $params->sort_by : 'orders.' . OrderDomainObject::getDefaultSort(), - direction: $params->sort_direction ?? 'desc', + column: 'orders.' . $sortBy, + direction: $this->validateSortDirection($params->sort_direction, OrderDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/ProductCategoryRepository.php b/backend/app/Repository/Eloquent/ProductCategoryRepository.php index ab845cc061..0e39778c95 100644 --- a/backend/app/Repository/Eloquent/ProductCategoryRepository.php +++ b/backend/app/Repository/Eloquent/ProductCategoryRepository.php @@ -36,10 +36,10 @@ public function findByEventId(int $eventId, QueryParamsDTO $queryParamsDTO): Col } } - // Apply sorting from QueryParamsDTO - if (!empty($queryParamsDTO->sort_by)) { - $query->orderBy($queryParamsDTO->sort_by, $queryParamsDTO->sort_direction ?? 'asc'); - } + $query->orderBy( + $this->validateSortColumn($queryParamsDTO->sort_by, ProductCategoryDomainObject::class), + $this->validateSortDirection($queryParamsDTO->sort_direction, ProductCategoryDomainObject::class), + ); return $query->get(); } diff --git a/backend/app/Repository/Eloquent/ProductRepository.php b/backend/app/Repository/Eloquent/ProductRepository.php index c199b678a4..afed60387a 100644 --- a/backend/app/Repository/Eloquent/ProductRepository.php +++ b/backend/app/Repository/Eloquent/ProductRepository.php @@ -41,8 +41,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - $params->sort_by ?? ProductDomainObject::getDefaultSort(), - $params->sort_direction ?? ProductDomainObject::getDefaultSortDirection(), + $this->validateSortColumn($params->sort_by, ProductDomainObject::class), + $this->validateSortDirection($params->sort_direction, ProductDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/PromoCodeRepository.php b/backend/app/Repository/Eloquent/PromoCodeRepository.php index 376440c865..3a93e91a8b 100644 --- a/backend/app/Repository/Eloquent/PromoCodeRepository.php +++ b/backend/app/Repository/Eloquent/PromoCodeRepository.php @@ -39,8 +39,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - column: $params->sort_by ?? PromoCodeDomainObject::getDefaultSort(), - direction: $params->sort_direction ?? 'desc', + column: $this->validateSortColumn($params->sort_by, PromoCodeDomainObject::class), + direction: $this->validateSortDirection($params->sort_direction, PromoCodeDomainObject::class), ); return $this->paginateWhere( diff --git a/backend/app/Repository/Eloquent/WaitlistEntryRepository.php b/backend/app/Repository/Eloquent/WaitlistEntryRepository.php index bb27bf8c5d..25af52903c 100644 --- a/backend/app/Repository/Eloquent/WaitlistEntryRepository.php +++ b/backend/app/Repository/Eloquent/WaitlistEntryRepository.php @@ -156,8 +156,8 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware } $this->model = $this->model->orderBy( - column: $params->sort_by ?? WaitlistEntryDomainObject::getDefaultSort(), - direction: $params->sort_direction ?? WaitlistEntryDomainObject::getDefaultSortDirection(), + column: $this->validateSortColumn($params->sort_by, WaitlistEntryDomainObject::class), + direction: $this->validateSortDirection($params->sort_direction, WaitlistEntryDomainObject::class), ); return $this->loadRelation(new Relationship( diff --git a/backend/app/Services/Domain/Event/DuplicateEventService.php b/backend/app/Services/Domain/Event/DuplicateEventService.php index f7c0bdef03..22326cb041 100644 --- a/backend/app/Services/Domain/Event/DuplicateEventService.php +++ b/backend/app/Services/Domain/Event/DuplicateEventService.php @@ -249,6 +249,7 @@ private function clonePerOrderQuestions(EventDomainObject $event, int $newEventI $this->createQuestionService->createQuestion( (new QuestionDomainObject()) ->setTitle($question->getTitle()) + ->setDescription($question->getDescription()) ->setEventId($newEventId) ->setBelongsTo($question->getBelongsTo()) ->setType($question->getType()) diff --git a/backend/app/Validators/Rules/NoInternalUrlRule.php b/backend/app/Validators/Rules/NoInternalUrlRule.php index 5e3360ef87..a24c1df239 100644 --- a/backend/app/Validators/Rules/NoInternalUrlRule.php +++ b/backend/app/Validators/Rules/NoInternalUrlRule.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Contracts\Validation\ValidationRule; +use Illuminate\Support\Facades\Config; class NoInternalUrlRule implements ValidationRule { @@ -52,6 +53,11 @@ public function validate(string $attribute, mixed $value, Closure $fail): void $host = substr($host, 1, -1); } + // Handle NoInternalIP/Host Exceptions + if ($this->isWhitelistedHost($host)) { + return; + } + if ($this->isBlockedHost($host)) { $fail(__('The :attribute cannot point to localhost or internal addresses.')); return; @@ -145,4 +151,16 @@ private function resolveAndNormalize(string $host): string|false return $ip; } -} + + private function isWhitelistedHost(string $host): bool + { + $whitelistedHosts = Config::string('app.allowed_internal_webhook_hosts'); + if (!empty($whitelistedHosts)) { + $allowedList = array_filter(array_map('trim', explode(',', $whitelistedHosts))); + if (in_array($host, $allowedList) || in_array(gethostbyname($host), $allowedList)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/backend/composer.json b/backend/composer.json index 90341cb29d..5da204fd6b 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -4,7 +4,7 @@ "description": "hi.events - Ticket selling and event management.", "keywords": ["ticketing", "events"], "license": "AGPL-3.0", - "version": "1.7.0-beta", + "version": "v1.7.1-beta", "require": { "php": "^8.2", "ext-intl": "*", diff --git a/backend/config/app.php b/backend/config/app.php index f5c6bcbaa1..50bcabc3e9 100644 --- a/backend/config/app.php +++ b/backend/config/app.php @@ -22,6 +22,7 @@ 'stripe_connect_account_type' => env('APP_STRIPE_CONNECT_ACCOUNT_TYPE', 'express'), 'platform_support_email' => env('APP_PLATFORM_SUPPORT_EMAIL', 'support@example.com'), 'enforce_email_confirmation_during_registration' => env('APP_ENFORCE_EMAIL_CONFIRMATION_DURING_REGISTRATION', false), + 'allowed_internal_webhook_hosts' => env('APP_ALLOWED_INTERNAL_WEBHOOK_HOSTS', ''), /** * The number of page views to batch before updating the database diff --git a/frontend/package.json b/frontend/package.json index 6a4d8653a1..4c3c3433ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "hievents-frontend", "private": true, - "version": "1.7.0-beta", + "version": "v1.7.1-beta", "type": "module", "scripts": { "dev:csr": "vite --port 5678 --host 0.0.0.0",