1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* The page and form callbacks for use in the checkout form.
|
6
|
*/
|
7
|
|
8
|
|
9
|
/**
|
10
|
* Redirects invalid checkout attempts or displays the checkout form if valid.
|
11
|
*/
|
12
|
function commerce_checkout_router($order, $checkout_page = NULL) {
|
13
|
$checkout_pages = commerce_checkout_pages();
|
14
|
|
15
|
// If no checkout page is specified, default to the first one.
|
16
|
if (empty($checkout_page)) {
|
17
|
$checkout_page = reset($checkout_pages);
|
18
|
}
|
19
|
|
20
|
// If the user does not have access to checkout the order, return a 404. We
|
21
|
// could return a 403, but then the user would know they've identified a
|
22
|
// potentially valid checkout URL.
|
23
|
if (!commerce_checkout_access($order)) {
|
24
|
return MENU_NOT_FOUND;
|
25
|
}
|
26
|
|
27
|
// If the user is attempting to access an inaccessible page for their order,
|
28
|
// redirect them to the proper page.
|
29
|
if (!commerce_checkout_page_access($checkout_page, $order)) {
|
30
|
$target_uri = commerce_checkout_order_uri($order);
|
31
|
|
32
|
// Only redirect if the target page is different from the page the user was
|
33
|
// trying to access. Otherwise give a 403 error.
|
34
|
if (!empty($target_uri) && $target_uri !== $_GET['q']) {
|
35
|
drupal_goto($target_uri);
|
36
|
}
|
37
|
else {
|
38
|
return MENU_ACCESS_DENIED;
|
39
|
}
|
40
|
}
|
41
|
|
42
|
// Ensure the order can proceed to checkout; if not, redirect away.
|
43
|
if (!commerce_checkout_order_can_checkout($order)) {
|
44
|
drupal_goto('<front>');
|
45
|
}
|
46
|
|
47
|
// Prior to displaying the checkout form, allow other modules to route the
|
48
|
// checkout form.
|
49
|
module_invoke_all('commerce_checkout_router', $order, $checkout_page);
|
50
|
|
51
|
// Update the page title if specified.
|
52
|
if (!empty($checkout_page['title'])) {
|
53
|
drupal_set_title($checkout_page['title']);
|
54
|
}
|
55
|
|
56
|
return drupal_get_form('commerce_checkout_form_' . $checkout_page['page_id'], $order, $checkout_page);
|
57
|
}
|
58
|
|
59
|
/**
|
60
|
* Builds the checkout form for the given order on the specified checkout page.
|
61
|
*
|
62
|
* @param $order
|
63
|
* The fully loaded order object being checked out.
|
64
|
* @param $checkout_page
|
65
|
* The checkout page object representing the current step in checkout.
|
66
|
*/
|
67
|
function commerce_checkout_form($form, &$form_state, $order, $checkout_page) {
|
68
|
global $user;
|
69
|
|
70
|
$form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') .'/theme/commerce_checkout.base.css';
|
71
|
$form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') .'/theme/commerce_checkout.theme.css';
|
72
|
$form['#attached']['js'][] = drupal_get_path('module', 'commerce_checkout') . '/commerce_checkout.js';
|
73
|
|
74
|
$form_state['order'] = $order;
|
75
|
$form_state['checkout_page'] = $checkout_page;
|
76
|
$form_state['account'] = clone($user);
|
77
|
|
78
|
// Add any help text that has been defined for this checkout page.
|
79
|
$help = filter_xss($checkout_page['help']);
|
80
|
|
81
|
if (!empty($help)) {
|
82
|
$form['help'] = array(
|
83
|
'#markup' => theme('commerce_checkout_help', array('help' => $help)),
|
84
|
);
|
85
|
}
|
86
|
|
87
|
// Restore form errors.
|
88
|
if (!empty($form_state['storage']['errors'])) {
|
89
|
$form_errors = &drupal_static('form_set_error', array());
|
90
|
$form_errors = $form_state['storage']['errors'];
|
91
|
}
|
92
|
|
93
|
$form['#after_build'][] = 'commerce_checkout_form_process_errors';
|
94
|
|
95
|
// Catch and clear already pushed messages.
|
96
|
$previous_messages = drupal_get_messages();
|
97
|
$show_errors_message = FALSE;
|
98
|
$visible_panes = 0;
|
99
|
|
100
|
// Add any enabled checkout panes for this page to the form.
|
101
|
foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page['page_id'])) as $pane_id => $checkout_pane) {
|
102
|
if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form')) {
|
103
|
// Generate the pane form.
|
104
|
$pane_form = $callback($form, $form_state, $checkout_pane, $order);
|
105
|
|
106
|
// Combine the messages that were created during this pane's validation or
|
107
|
// submit process with any that were created during the pane generation
|
108
|
// and merge them into the session's current messages array.
|
109
|
if (!empty($form_state['storage']['messages'][$pane_id])) {
|
110
|
$_SESSION['messages'] = array_merge_recursive($form_state['storage']['messages'][$pane_id], drupal_get_messages());
|
111
|
}
|
112
|
|
113
|
// If there are messages in the session right now for this pane, theme
|
114
|
// them into the form right above the pane itself.
|
115
|
if (!empty($_SESSION['messages'])) {
|
116
|
// If there are error messages and this is not the first pane on the
|
117
|
// form, then indicate we need to show an error message at the top of
|
118
|
// the page.
|
119
|
if ($visible_panes > 0 && !empty($_SESSION['messages']['error'])) {
|
120
|
$show_errors_message = TRUE;
|
121
|
}
|
122
|
|
123
|
// Rendering status messages clears the session of messages, so they
|
124
|
// will not be visible if the user is redirected. We can at least not
|
125
|
// render here when we detect the global variable added by Rules to
|
126
|
// handle redirects, though modules implementing redirects will still
|
127
|
// encounter the same problem of "lost" messages.
|
128
|
if (!isset($GLOBALS['_rules_action_drupal_goto_do'])){
|
129
|
$form_state['storage']['themed_messages'][$pane_id] = theme('status_messages');
|
130
|
|
131
|
$pane_form[$pane_id . '_messages'] = array(
|
132
|
'#markup' => $form_state['storage']['themed_messages'][$pane_id],
|
133
|
'#weight' => -50,
|
134
|
);
|
135
|
}
|
136
|
}
|
137
|
|
138
|
// Create a fieldset for the pane and add the form data defined in the
|
139
|
// pane's form callback.
|
140
|
if ($pane_form) {
|
141
|
$form[$pane_id] = $pane_form + array(
|
142
|
'#type' => $checkout_pane['fieldset'] ? 'fieldset' : 'container',
|
143
|
'#title' => check_plain($checkout_pane['title']),
|
144
|
'#collapsible' => $checkout_pane['collapsible'],
|
145
|
'#collapsed' => $checkout_pane['collapsed'],
|
146
|
'#attributes' => array('class' => array($pane_id)),
|
147
|
'#tree' => TRUE,
|
148
|
);
|
149
|
|
150
|
$visible_panes++;
|
151
|
}
|
152
|
}
|
153
|
}
|
154
|
|
155
|
// Restore general messages to the current session's messages array.
|
156
|
$_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
|
157
|
|
158
|
// If there are errors on the form, add a message to the top of the page.
|
159
|
if ($show_errors_message) {
|
160
|
$form['error_message'] = array(
|
161
|
'#markup' => theme('commerce_checkout_errors_message', array('label' => t('Errors on form'), 'message' => t('There are errors on the page. Please correct them and resubmit the form.'))),
|
162
|
'#weight' => -10,
|
163
|
);
|
164
|
}
|
165
|
|
166
|
// Only add buttons to the form if the checkout page hasn't disabled them.
|
167
|
if ($checkout_page['buttons']) {
|
168
|
$form['buttons'] = array(
|
169
|
'#type' => 'fieldset',
|
170
|
'#attributes' => array('class' => array('checkout-buttons')),
|
171
|
);
|
172
|
$form['buttons']['continue'] = array(
|
173
|
'#type' => 'submit',
|
174
|
'#value' => $checkout_page['submit_value'],
|
175
|
'#attributes' => array('class' => array('checkout-continue')),
|
176
|
'#suffix' => '<span class="checkout-processing element-invisible"></span>',
|
177
|
'#validate' => array('commerce_checkout_form_validate'),
|
178
|
'#submit' => array('commerce_checkout_form_submit'),
|
179
|
);
|
180
|
|
181
|
// Add the cancel or back button where appropriate. We define button level
|
182
|
// submit handlers because we're using hook_forms() to use this form builder
|
183
|
// function and to avoid issues if other modules implement button level submit
|
184
|
// handlers on these or custom checkout buttons.
|
185
|
$button_operator = '<span class="button-operator">' . t('or') . '</span>';
|
186
|
|
187
|
if (!$checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
|
188
|
// Add an empty "Back" button value to avoid submission errors.
|
189
|
$form['buttons']['back'] = array(
|
190
|
'#type' => 'value',
|
191
|
'#value' => '',
|
192
|
);
|
193
|
|
194
|
// Store the cancel redirect in the form so modules can modify it easily.
|
195
|
$form_state['cancel_redirect'] = '<front>';
|
196
|
|
197
|
$form['buttons']['cancel'] = array(
|
198
|
'#type' => 'submit',
|
199
|
'#value' => t('Cancel'),
|
200
|
'#attributes' => array('class' => array('checkout-cancel')),
|
201
|
'#submit' => array('commerce_checkout_form_cancel_submit'),
|
202
|
'#limit_validation_errors' => array(),
|
203
|
'#prefix' => $button_operator,
|
204
|
);
|
205
|
}
|
206
|
elseif ($checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
|
207
|
$form['buttons']['back'] = array(
|
208
|
'#type' => 'submit',
|
209
|
'#value' => $checkout_page['back_value'],
|
210
|
'#attributes' => array('class' => array('checkout-back')),
|
211
|
'#submit' => array('commerce_checkout_form_back_submit'),
|
212
|
'#limit_validation_errors' => array(),
|
213
|
'#prefix' => $button_operator,
|
214
|
);
|
215
|
}
|
216
|
}
|
217
|
|
218
|
// Remove form level validate and submit handlers.
|
219
|
$form['#validate'] = array();
|
220
|
$form['#submit'] = array();
|
221
|
|
222
|
return $form;
|
223
|
}
|
224
|
|
225
|
/**
|
226
|
* After build callback for the checkout form.
|
227
|
*/
|
228
|
function commerce_checkout_form_process_errors($form, $form_state) {
|
229
|
// Do this only on form rebuild (when the form will not be validated anymore):
|
230
|
if (!empty($form_state['storage']['errors']) && !empty($form_state['rebuild'])) {
|
231
|
foreach (array_keys($form_state['storage']['errors']) as $element_name) {
|
232
|
// Look for all elements which have $element_name as parents, and
|
233
|
// restore their #validated property (so _form_set_class() will set
|
234
|
// the error class even though the rebuilt form is not validated).
|
235
|
// We can't simply use drupal_array_get_nested_value(), since the #parents
|
236
|
// property may have been changed and not match the form structure.
|
237
|
_commerce_checkout_set_validated($form, $element_name);
|
238
|
}
|
239
|
}
|
240
|
|
241
|
return $form;
|
242
|
}
|
243
|
|
244
|
/**
|
245
|
* Set '#validated' on elements which have the specified parents.
|
246
|
*/
|
247
|
function _commerce_checkout_set_validated(&$element, $imploded_parents) {
|
248
|
// Recurse to child elements if the current element is a container.
|
249
|
foreach (element_children($element) as $key) {
|
250
|
_commerce_checkout_set_validated($element[$key], $imploded_parents);
|
251
|
}
|
252
|
|
253
|
// This will also set #validated on all elements where #needs_validation would
|
254
|
// be FALSE, but that doesn't hurt anything.
|
255
|
if (!empty($element['#parents']) && strpos($imploded_parents, implode('][', $element['#parents'])) === 0) {
|
256
|
$element['#validated'] = TRUE;
|
257
|
}
|
258
|
}
|
259
|
|
260
|
/**
|
261
|
* Validate handler for the continue button of the checkout form.
|
262
|
*
|
263
|
* This function calls the validation function of each pane, followed by
|
264
|
* the submit function if the validation succeeded. As long as one pane
|
265
|
* fails validation, we then ask for the form to be rebuilt. Once all the panes
|
266
|
* are happy, we move on to the next page.
|
267
|
*/
|
268
|
function commerce_checkout_form_validate($form, &$form_state) {
|
269
|
$checkout_page = $form_state['checkout_page'];
|
270
|
|
271
|
// Load a fresh copy of the order stored in the form.
|
272
|
$order = commerce_order_load($form_state['order']->order_id);
|
273
|
|
274
|
// Catch and clear already pushed messages.
|
275
|
$previous_messages = drupal_get_messages();
|
276
|
|
277
|
// Load any pre-existing validation errors for the elements.
|
278
|
$errors = array();
|
279
|
|
280
|
foreach ((array) form_get_errors() as $element_path => $error) {
|
281
|
list($pane_id, ) = explode('][', $element_path, 2);
|
282
|
$errors[$pane_id][$element_path] = $error;
|
283
|
}
|
284
|
|
285
|
// Loop through the enabled checkout panes for the current page.
|
286
|
$form_validate = TRUE;
|
287
|
foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page['page_id'])) as $pane_id => $checkout_pane) {
|
288
|
$validate = TRUE;
|
289
|
|
290
|
// If any element in the pane failed validation, we mark the pane as
|
291
|
// unvalidated and replay the validation messages on top of it.
|
292
|
if (!empty($errors[$pane_id])) {
|
293
|
$validate = FALSE;
|
294
|
|
295
|
foreach ($errors[$pane_id] as $element_path => $message) {
|
296
|
if ($message) {
|
297
|
drupal_set_message($message, 'error');
|
298
|
}
|
299
|
}
|
300
|
|
301
|
if (isset($previous_messages['error'])) {
|
302
|
$previous_messages['error'] = array_values(array_diff($previous_messages['error'], $errors[$pane_id]));
|
303
|
}
|
304
|
}
|
305
|
|
306
|
// If the pane has defined a checkout form validate handler...
|
307
|
if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_validate')) {
|
308
|
// Give it a chance to process the submitted data.
|
309
|
$validate &= $callback($form, $form_state, $checkout_pane, $order);
|
310
|
}
|
311
|
|
312
|
// Catch and clear panes' messages.
|
313
|
$pane_messages = drupal_get_messages();
|
314
|
|
315
|
// Submit the pane if it validated.
|
316
|
if ($validate && $callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_submit')) {
|
317
|
$callback($form, $form_state, $checkout_pane, $order);
|
318
|
}
|
319
|
|
320
|
// Generate status messages.
|
321
|
$form_state['storage']['messages'][$pane_id] = array_merge_recursive($pane_messages, drupal_get_messages());
|
322
|
|
323
|
// A failed pane makes the form fail.
|
324
|
$form_validate &= $validate;
|
325
|
}
|
326
|
|
327
|
// Restore messages and form errors.
|
328
|
$_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
|
329
|
$form_errors = &drupal_static('form_set_error', array());
|
330
|
$form_state['storage']['errors'] = $form_errors;
|
331
|
$form_errors = array();
|
332
|
|
333
|
// Save the updated order object and reset the order in the form cache to
|
334
|
// ensure rebuilt forms use the updated order.
|
335
|
commerce_order_save($order);
|
336
|
$form_state['build_info']['args'][0] = $order;
|
337
|
|
338
|
// If a pane failed validation or the form state has otherwise been altered to
|
339
|
// initiate a rebuild, return without moving to the next checkout page.
|
340
|
if (!$form_validate || $form_state['rebuild']) {
|
341
|
$form_state['rebuild'] = TRUE;
|
342
|
}
|
343
|
}
|
344
|
|
345
|
/**
|
346
|
* Submit handler for the continue button of the checkout form.
|
347
|
*/
|
348
|
function commerce_checkout_form_submit($form, &$form_state) {
|
349
|
$checkout_page = $form_state['checkout_page'];
|
350
|
|
351
|
// Load a fresh copy of the order stored in the form.
|
352
|
$order = commerce_order_load($form_state['order']->order_id);
|
353
|
|
354
|
// If we are going to redirect with checkout pane messages stored in the form
|
355
|
// state, they will not be displayed on a subsequent form build like normal.
|
356
|
// Move them out of the form state messages array and into the current
|
357
|
// session's general message array instead.
|
358
|
if (!empty($form_state['storage']['messages'])) {
|
359
|
foreach ($form_state['storage']['messages'] as $pane_id => $pane_messages) {
|
360
|
$_SESSION['messages'] = array_merge_recursive($_SESSION['messages'], $pane_messages);
|
361
|
}
|
362
|
}
|
363
|
|
364
|
// If the form was submitted via the continue button...
|
365
|
if (end($form_state['triggering_element']['#array_parents']) == 'continue') {
|
366
|
// If there is another checkout page...
|
367
|
if ($checkout_page['next_page']) {
|
368
|
// Update the order status to reflect the next checkout page.
|
369
|
$order = commerce_order_status_update($order, 'checkout_' . $checkout_page['next_page'], FALSE, NULL, t('Customer continued to the next checkout page via a submit button.'));
|
370
|
|
371
|
// If it happens to be the complete page, process completion now.
|
372
|
if ($checkout_page['next_page'] == 'complete') {
|
373
|
commerce_checkout_complete($order);
|
374
|
}
|
375
|
|
376
|
// Redirect to the next checkout page.
|
377
|
$form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $checkout_page['next_page'];
|
378
|
}
|
379
|
}
|
380
|
}
|
381
|
|
382
|
/**
|
383
|
* Special submit handler for the back button to avoid processing orders.
|
384
|
*/
|
385
|
function commerce_checkout_form_back_submit($form, &$form_state) {
|
386
|
// If there is a previous page...
|
387
|
if ($previous_page = commerce_checkout_page_load($form_state['checkout_page']['prev_page'])) {
|
388
|
$order = commerce_order_load($form_state['order']->order_id);
|
389
|
|
390
|
// Move the form back to that page.
|
391
|
if ($previous_page['prev_page']) {
|
392
|
$form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $previous_page['page_id'];
|
393
|
}
|
394
|
else {
|
395
|
$form_state['redirect'] = 'checkout/' . $order->order_id;
|
396
|
}
|
397
|
|
398
|
// Update the order status for the checkout step.
|
399
|
$form_state['order'] = commerce_order_status_update($order, 'checkout_' . $previous_page['page_id'], FALSE, NULL, t('Customer returned to the previous checkout page via a submit button.'));
|
400
|
}
|
401
|
}
|
402
|
|
403
|
/**
|
404
|
* Special submit handler for the cancel button to avoid processing orders.
|
405
|
*/
|
406
|
function commerce_checkout_form_cancel_submit($form, &$form_state) {
|
407
|
$order = commerce_order_load($form_state['order']->order_id);
|
408
|
|
409
|
// Set the order status back to the first checkout page's status.
|
410
|
$order_state = commerce_order_state_load('checkout');
|
411
|
$form_state['order'] = commerce_order_status_update($order, $order_state['default_status'], TRUE);
|
412
|
|
413
|
// Skip saving in the status update and manually save here to force a save
|
414
|
// even when the status doesn't actually change.
|
415
|
if (variable_get('commerce_order_auto_revision', TRUE)) {
|
416
|
$form_state['order']->revision = TRUE;
|
417
|
$form_state['order']->log = t('Customer manually canceled the checkout process.');
|
418
|
}
|
419
|
|
420
|
commerce_order_save($form_state['order']);
|
421
|
|
422
|
drupal_set_message(t('Checkout of your current order has been canceled and may be resumed when you are ready.'));
|
423
|
|
424
|
$form_state['redirect'] = $form_state['cancel_redirect'];
|
425
|
}
|
426
|
|
427
|
/**
|
428
|
* Themes the optional checkout review page data.
|
429
|
*/
|
430
|
function theme_commerce_checkout_review($variables) {
|
431
|
$form = $variables['form'];
|
432
|
|
433
|
// Turn the review data array into table rows.
|
434
|
$rows = array();
|
435
|
|
436
|
foreach ($form['#data'] as $pane_id => $data) {
|
437
|
// First add a row for the title.
|
438
|
$rows[] = array(
|
439
|
'data' => array(
|
440
|
array('data' => $data['title'], 'colspan' => 2),
|
441
|
),
|
442
|
'class' => array('pane-title', 'odd'),
|
443
|
);
|
444
|
|
445
|
// Next, add the data for this particular section.
|
446
|
if (is_array($data['data'])) {
|
447
|
// If it's an array, treat each key / value pair as a 2 column row.
|
448
|
foreach ($data['data'] as $key => $value) {
|
449
|
$rows[] = array(
|
450
|
'data' => array(
|
451
|
array('data' => $key .':', 'class' => array('pane-data-key')),
|
452
|
array('data' => $value, 'class' => array('pane-data-value')),
|
453
|
),
|
454
|
'class' => array('pane-data', 'even'),
|
455
|
);
|
456
|
}
|
457
|
}
|
458
|
else {
|
459
|
// Otherwise treat it as a block of text in its own row.
|
460
|
$rows[] = array(
|
461
|
'data' => array(
|
462
|
array('data' => $data['data'], 'colspan' => 2, 'class' => array('pane-data-full')),
|
463
|
),
|
464
|
'class' => array('pane-data', 'even'),
|
465
|
);
|
466
|
}
|
467
|
}
|
468
|
|
469
|
return theme('table', array('rows' => $rows, 'attributes' => array('class' => array('checkout-review'))));
|
470
|
}
|