88namespace craft \commerce \controllers ;
99
1010use Craft ;
11+ use craft \commerce \elements \Order ;
1112use craft \commerce \helpers \Locale ;
1213use craft \commerce \Plugin ;
13- use HttpInvalidParamException ;
14+ use craft \helpers \UrlHelper ;
15+ use craft \web \View ;
1416use Throwable ;
1517use yii \base \Exception ;
1618use yii \base \InvalidCallException ;
19+ use yii \web \BadRequestHttpException ;
1720use yii \web \HttpException ;
1821use yii \web \RangeNotSatisfiableHttpException ;
1922use yii \web \Response ;
2629 */
2730class DownloadsController extends BaseFrontEndController
2831{
32+ /**
33+ * Renders the email challenge template with the provided parameters.
34+ *
35+ * @param Order $order The order to display
36+ * @param string $orderNumber The order number
37+ * @param string|null $pdfHandle The PDF handle
38+ * @param string $option The PDF option
39+ * @param bool $inline Whether to display inline
40+ * @param array $errors Optional errors to display
41+ * @param string|null $email Optional email value to pre-fill
42+ * @return Response
43+ * @since 4.9.5
44+ */
45+ private function renderEmailChallenge (
46+ Order $ order ,
47+ string $ orderNumber ,
48+ ?string $ pdfHandle ,
49+ string $ option ,
50+ bool $ inline ,
51+ array $ errors = [],
52+ ?string $ email = null ,
53+ ): Response {
54+ $ params = [
55+ 'order ' => $ order ,
56+ 'orderNumber ' => $ orderNumber ,
57+ 'pdfHandle ' => $ pdfHandle ,
58+ 'option ' => $ option ,
59+ 'inline ' => $ inline ,
60+ ];
61+
62+ if (!empty ($ errors )) {
63+ $ params ['errors ' ] = $ errors ;
64+ }
65+
66+ if ($ email !== null ) {
67+ $ params ['email ' ] = $ email ;
68+ }
69+
70+ return $ this ->renderTemplate ('commerce/_downloads/email-challenge ' , $ params , View::TEMPLATE_MODE_CP );
71+ }
72+
2973 /**
3074 * @throws HttpException
3175 * @throws Throwable
@@ -38,9 +82,10 @@ public function actionPdf(): Response
3882 $ pdfHandle = $ this ->request ->getQueryParam ('pdfHandle ' );
3983 $ option = $ this ->request ->getQueryParam ('option ' , '' );
4084 $ inline = (bool ) $ this ->request ->getQueryParam ('inline ' , false );
85+ $ token = $ this ->request ->getQueryParam ('token ' );
4186
4287 if (!$ number ) {
43- throw new HttpInvalidParamException ('Order number required ' );
88+ throw new BadRequestHttpException ('Order number required ' );
4489 }
4590
4691 $ order = Plugin::getInstance ()->getOrders ()->getOrderByNumber ($ number );
@@ -49,6 +94,77 @@ public function actionPdf(): Response
4994 throw new HttpException (404 , 'Order not found ' );
5095 }
5196
97+ // Don't allow PDF downloads for carts without an email
98+ if (!$ order ->getEmail ()) {
99+ throw new HttpException (404 , 'Order not found ' );
100+ }
101+
102+ $ currentUser = Craft::$ app ->getUser ()->getIdentity ();
103+ $ hasValidToken = false ;
104+
105+ // Check if token is provided and valid (works for anyone, logged in or not)
106+ if ($ token ) {
107+ $ tokenData = Craft::$ app ->getTokens ()->getTokenRoute ($ token );
108+
109+ // Validate token structure and order number
110+ if (!$ tokenData || !isset ($ tokenData [1 ]['orderNumber ' ]) || $ tokenData [1 ]['orderNumber ' ] !== $ number ) {
111+ // Invalid token - redirect to challenge form with error
112+ Craft::$ app ->getSession ()->setError (Craft::t ('commerce ' , 'The download link is invalid. Please request a new one. ' ));
113+ return $ this ->redirect (UrlHelper::actionUrl ('commerce/downloads/email-challenge ' , [
114+ 'number ' => $ number ,
115+ 'pdfHandle ' => $ pdfHandle ,
116+ 'option ' => $ option ,
117+ 'inline ' => $ inline ,
118+ ]));
119+ }
120+
121+ // Check if token has expired based on the timestamp in the token data
122+ if (isset ($ tokenData [1 ]['expiresAt ' ])) {
123+ $ expiresAt = $ tokenData [1 ]['expiresAt ' ];
124+ $ now = (new \DateTime ())->getTimestamp ();
125+
126+ if ($ now > $ expiresAt ) {
127+ // Token expired - redirect to email challenge form
128+ return $ this ->redirect (UrlHelper::actionUrl ('commerce/downloads/email-challenge ' , [
129+ 'number ' => $ number ,
130+ 'pdfHandle ' => $ pdfHandle ,
131+ 'option ' => $ option ,
132+ 'inline ' => $ inline ,
133+ ]));
134+ }
135+ }
136+
137+ // Token is valid
138+ $ hasValidToken = true ;
139+ }
140+
141+ // Check user permissions if no valid token
142+ if (!$ hasValidToken ) {
143+ if ($ currentUser ) {
144+ // Check if user is the order customer, admin, or has permission to manage orders
145+ $ isOrderCustomer = $ order ->getCustomer () && $ order ->getCustomer ()->id === $ currentUser ->id ;
146+ $ hasPermission = $ currentUser ->admin || $ order ->canView ($ currentUser );
147+
148+ if (!($ isOrderCustomer || $ hasPermission )) {
149+ // Logged-in user without permission - redirect to email challenge form
150+ return $ this ->redirect (UrlHelper::actionUrl ('commerce/downloads/email-challenge ' , [
151+ 'number ' => $ number ,
152+ 'pdfHandle ' => $ pdfHandle ,
153+ 'option ' => $ option ,
154+ 'inline ' => $ inline ,
155+ ]));
156+ }
157+ } else {
158+ // Anonymous user without valid token - redirect to email challenge form
159+ return $ this ->redirect (UrlHelper::actionUrl ('commerce/downloads/email-challenge ' , [
160+ 'number ' => $ number ,
161+ 'pdfHandle ' => $ pdfHandle ,
162+ 'option ' => $ option ,
163+ 'inline ' => $ inline ,
164+ ]));
165+ }
166+ }
167+
52168 if ($ pdfHandle ) {
53169 $ pdf = Plugin::getInstance ()->getPdfs ()->getPdfByHandle ($ pdfHandle );
54170
@@ -84,4 +200,119 @@ public function actionPdf(): Response
84200 'inline ' => $ inline ,
85201 ]);
86202 }
203+
204+ /**
205+ * Displays the email challenge form for anonymous users trying to download an order PDF
206+ *
207+ * @return Response
208+ * @throws HttpException
209+ */
210+ public function actionEmailChallenge (): Response
211+ {
212+ $ number = $ this ->request ->getQueryParam ('number ' );
213+ $ pdfHandle = $ this ->request ->getQueryParam ('pdfHandle ' );
214+ $ option = $ this ->request ->getQueryParam ('option ' , '' );
215+ $ inline = (bool ) $ this ->request ->getQueryParam ('inline ' , false );
216+
217+ if (!$ number ) {
218+ throw new BadRequestHttpException ('Order number required ' );
219+ }
220+
221+ $ order = Plugin::getInstance ()->getOrders ()->getOrderByNumber ($ number );
222+
223+ if (!$ order ) {
224+ throw new HttpException (404 , 'Order not found ' );
225+ }
226+
227+ // Don't allow PDF downloads for carts without an email
228+ if (!$ order ->getEmail ()) {
229+ throw new HttpException (404 , 'Order not found ' );
230+ }
231+
232+ return $ this ->renderEmailChallenge ($ order , $ number , $ pdfHandle , $ option , $ inline );
233+ }
234+
235+ /**
236+ * Handles the email challenge form submission for anonymous users trying to download an order PDF
237+ *
238+ * @throws HttpException
239+ * @throws Exception
240+ */
241+ public function actionPdfChallenge (): Response
242+ {
243+ $ this ->requirePostRequest ();
244+
245+ $ orderNumberHash = $ this ->request ->getBodyParam ('orderNumberHash ' );
246+ $ pdfHandle = $ this ->request ->getBodyParam ('pdfHandle ' );
247+ $ option = $ this ->request ->getBodyParam ('option ' , '' );
248+ $ inline = (bool ) $ this ->request ->getBodyParam ('inline ' , false );
249+
250+ if (!$ orderNumberHash ) {
251+ throw new BadRequestHttpException ('Order number hash is required ' );
252+ }
253+
254+ // Validate the order number hash
255+ $ orderNumber = Craft::$ app ->getSecurity ()->validateData ($ orderNumberHash );
256+
257+ if ($ orderNumber === false ) {
258+ throw new BadRequestHttpException ('Invalid order number hash ' );
259+ }
260+
261+ $ order = Plugin::getInstance ()->getOrders ()->getOrderByNumber ($ orderNumber );
262+
263+ if (!$ order ) {
264+ throw new HttpException (404 , 'Order not found ' );
265+ }
266+
267+ // Build the download URL with the token using the Pdfs service
268+ $ downloadUrl = Plugin::getInstance ()->getPdfs ()->getPdfUrl ($ order , $ option , $ pdfHandle , $ inline );
269+
270+ // Send email using system message
271+ $ systemMessage = Craft::$ app ->getSystemMessages ()->getMessage ('commerce_pdf_download ' , $ order ->getOrderSite ()->language );
272+
273+ if (!Craft::$ app ->getMailer ()->composeFromKey ('commerce_pdf_download ' , [
274+ 'link ' => $ downloadUrl ,
275+ 'order ' => $ order ,
276+ ])->setTo ($ order ->email )->send ()) {
277+ Craft::$ app ->getSession ()->setError (Craft::t ('commerce ' , 'Failed to send email. Please try again. ' ));
278+ return $ this ->renderEmailChallenge ($ order , $ orderNumber , $ pdfHandle , $ option , $ inline );
279+ }
280+
281+ Craft::$ app ->getSession ()->setNotice (Craft::t ('commerce ' , 'A new download link has been sent to {email} ' , ['email ' => $ order ->getMaskedEmail ()]));
282+
283+ // Redirect to success page to prevent duplicate submissions on refresh
284+ return $ this ->redirect (UrlHelper::actionUrl ('commerce/downloads/pdf-sent ' , ['hash ' => $ orderNumberHash ]));
285+ }
286+
287+ /**
288+ * Displays the success page after email challenge is completed
289+ *
290+ * @return Response
291+ * @throws HttpException
292+ */
293+ public function actionPdfSent (): Response
294+ {
295+ $ orderNumberHash = $ this ->request ->getQueryParam ('hash ' );
296+
297+ if (!$ orderNumberHash ) {
298+ throw new BadRequestHttpException ('Hash parameter required ' );
299+ }
300+
301+ // Validate and extract the order number from the hash
302+ $ orderNumber = Craft::$ app ->getSecurity ()->validateData ($ orderNumberHash );
303+
304+ if ($ orderNumber === false ) {
305+ throw new HttpException (400 , 'Invalid hash parameter ' );
306+ }
307+
308+ $ order = Plugin::getInstance ()->getOrders ()->getOrderByNumber ($ orderNumber );
309+
310+ if (!$ order ) {
311+ throw new HttpException (404 , 'Order not found ' );
312+ }
313+
314+ return $ this ->renderTemplate ('commerce/_downloads/email-sent ' , [
315+ 'email ' => $ order ->getMaskedEmail (),
316+ ], View::TEMPLATE_MODE_CP );
317+ }
87318}
0 commit comments