Projet

Général

Profil

Paste
Télécharger (18,1 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / commerce / modules / checkout / includes / commerce_checkout.pages.inc @ 9d13637e

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
  // Ensure this include file is loaded when the form is rebuilt from the cache.
71
  $form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_checkout') . '/includes/commerce_checkout.pages.inc';
72

    
73
  $form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') .'/theme/commerce_checkout.base.css';
74
  $form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') .'/theme/commerce_checkout.theme.css';
75
  $form['#attached']['js'][] = drupal_get_path('module', 'commerce_checkout') . '/commerce_checkout.js';
76

    
77
  $form_state['order'] = $order;
78
  $form_state['checkout_page'] = $checkout_page;
79
  $form_state['account'] = clone($user);
80

    
81
  // Add any help text that has been defined for this checkout page.
82
  $help = filter_xss($checkout_page['help']);
83

    
84
  if (!empty($help)) {
85
    $form['help'] = array(
86
      '#markup' => theme('commerce_checkout_help', array('help' => $help)),
87
    );
88
  }
89

    
90
  // Restore form errors.
91
  if (!empty($form_state['storage']['errors'])) {
92
    $form_errors = &drupal_static('form_set_error', array());
93
    $form_errors = $form_state['storage']['errors'];
94
  }
95

    
96
  $form['#after_build'][] = 'commerce_checkout_form_process_errors';
97

    
98
  // Catch and clear already pushed messages.
99
  $previous_messages = drupal_get_messages();
100
  $show_errors_message = FALSE;
101
  $visible_panes = 0;
102

    
103
  // Add any enabled checkout panes for this page to the form.
104
  foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page['page_id'])) as $pane_id => $checkout_pane) {
105
    if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form')) {
106
      // Generate the pane form.
107
      $pane_form = $callback($form, $form_state, $checkout_pane, $order);
108

    
109
      // Combine the messages that were created during this pane's validation or
110
      // submit process with any that were created during the pane generation
111
      // and merge them into the session's current messages array.
112
      if (!empty($form_state['storage']['messages'][$pane_id])) {
113
        $_SESSION['messages'] = array_merge_recursive($form_state['storage']['messages'][$pane_id], drupal_get_messages());
114
      }
115

    
116
      // If there are messages in the session right now for this pane, theme
117
      // them into the form right above the pane itself.
118
      if (!empty($_SESSION['messages'])) {
119
        // If there are error messages and this is not the first pane on the
120
        // form, then indicate we need to show an error message at the top of
121
        // the page.
122
        if ($visible_panes > 0 && !empty($_SESSION['messages']['error'])) {
123
          $show_errors_message = TRUE;
124
        }
125

    
126
        // Rendering status messages clears the session of messages, so they
127
        // will not be visible if the user is redirected. We can at least not
128
        // render here when we detect the global variable added by Rules to
129
        // handle redirects, though modules implementing redirects will still
130
        // encounter the same problem of "lost" messages.
131
        if (!isset($GLOBALS['_rules_action_drupal_goto_do'])){
132
          $form_state['storage']['themed_messages'][$pane_id] = theme('status_messages');
133

    
134
          $pane_form[$pane_id . '_messages'] = array(
135
            '#markup' => $form_state['storage']['themed_messages'][$pane_id],
136
            '#weight' => -50,
137
          );
138
        }
139
      }
140

    
141
      // Create a fieldset for the pane and add the form data defined in the
142
      // pane's form callback.
143
      if ($pane_form) {
144
        $form[$pane_id] = $pane_form + array(
145
          '#type' => $checkout_pane['fieldset'] ? 'fieldset' : 'container',
146
          '#title' => check_plain($checkout_pane['title']),
147
          '#collapsible' => $checkout_pane['collapsible'],
148
          '#collapsed' => $checkout_pane['collapsed'],
149
          '#attributes' => array('class' => array($pane_id)),
150
          '#tree' => TRUE,
151
        );
152

    
153
        $visible_panes++;
154
      }
155
    }
156
  }
157

    
158
  // Restore general messages to the current session's messages array.
159
  $_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
160

    
161
  // If there are errors on the form, add a message to the top of the page.
162
  if ($show_errors_message) {
163
    $form['error_message'] = array(
164
      '#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.'))),
165
      '#weight' => -10,
166
    );
167
  }
168

    
169
  // Only add buttons to the form if the checkout page hasn't disabled them.
170
  if ($checkout_page['buttons']) {
171
    $form['buttons'] = array(
172
      '#type' => 'fieldset',
173
      '#attributes' => array('class' => array('checkout-buttons')),
174
    );
175
    $form['buttons']['continue'] = array(
176
      '#type' => 'submit',
177
      '#value' => $checkout_page['submit_value'],
178
      '#attributes' => array('class' => array('checkout-continue')),
179
      '#suffix' => '<span class="checkout-processing element-invisible"></span>',
180
      '#validate' => array('commerce_checkout_form_validate'),
181
      '#submit' => array('commerce_checkout_form_submit'),
182
    );
183

    
184
    // Add the cancel or back button where appropriate. We define button level
185
    // submit handlers because we're using hook_forms() to use this form builder
186
    // function and to avoid issues if other modules implement button level submit
187
    // handlers on these or custom checkout buttons.
188
    $button_operator = '<span class="button-operator">' . t('or') . '</span>';
189

    
190
    if (!$checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
191
      // Add an empty "Back" button value to avoid submission errors.
192
      $form['buttons']['back'] = array(
193
        '#type' => 'value',
194
        '#value' => '',
195
      );
196

    
197
      // Store the cancel redirect in the form so modules can modify it easily.
198
      $form_state['cancel_redirect'] = '<front>';
199

    
200
      $form['buttons']['cancel'] = array(
201
        '#type' => 'submit',
202
        '#value' => t('Cancel'),
203
        '#attributes' => array('class' => array('checkout-cancel')),
204
        '#submit' => array('commerce_checkout_form_cancel_submit'),
205
        '#limit_validation_errors' => array(),
206
        '#prefix' => $button_operator,
207
      );
208
    }
209
    elseif ($checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
210
      $form['buttons']['back'] = array(
211
        '#type' => 'submit',
212
        '#value' => $checkout_page['back_value'],
213
        '#attributes' => array('class' => array('checkout-back')),
214
        '#submit' => array('commerce_checkout_form_back_submit'),
215
        '#limit_validation_errors' => array(),
216
        '#prefix' => $button_operator,
217
      );
218
    }
219
  }
220

    
221
  // Remove form level validate and submit handlers.
222
  $form['#validate'] = array();
223
  $form['#submit'] = array();
224

    
225
  return $form;
226
}
227

    
228
/**
229
 * After build callback for the checkout form.
230
 */
231
function commerce_checkout_form_process_errors($form, $form_state) {
232
  // Do this only on form rebuild (when the form will not be validated anymore):
233
  if (!empty($form_state['storage']['errors']) && !empty($form_state['rebuild'])) {
234
    foreach (array_keys($form_state['storage']['errors']) as $element_name) {
235
      // Look for all elements which have $element_name as parents, and
236
      // restore their #validated property (so _form_set_class() will set
237
      // the error class even though the rebuilt form is not validated).
238
      // We can't simply use drupal_array_get_nested_value(), since the #parents
239
      // property may have been changed and not match the form structure.
240
      _commerce_checkout_set_validated($form, $element_name);
241
    }
242
  }
243

    
244
  return $form;
245
}
246

    
247
/**
248
 * Set '#validated' on elements which have the specified parents.
249
 */
250
function _commerce_checkout_set_validated(&$element, $imploded_parents) {
251
  // Recurse to child elements if the current element is a container.
252
  foreach (element_children($element) as $key) {
253
    _commerce_checkout_set_validated($element[$key], $imploded_parents);
254
  }
255

    
256
  // This will also set #validated on all elements where #needs_validation would
257
  // be FALSE, but that doesn't hurt anything.
258
  if (!empty($element['#parents']) && strpos($imploded_parents, implode('][', $element['#parents'])) === 0) {
259
    $element['#validated'] = TRUE;
260
  }
261
}
262

    
263
/**
264
 * Validate handler for the continue button of the checkout form.
265
 *
266
 * This function calls the validation function of each pane, followed by
267
 * the submit function if the validation succeeded. As long as one pane
268
 * fails validation, we then ask for the form to be rebuilt. Once all the panes
269
 * are happy, we move on to the next page.
270
 */
271
function commerce_checkout_form_validate($form, &$form_state) {
272
  $checkout_page = $form_state['checkout_page'];
273

    
274
  // Load a fresh copy of the order stored in the form.
275
  $order = commerce_order_load($form_state['order']->order_id);
276

    
277
  // Catch and clear already pushed messages.
278
  $previous_messages = drupal_get_messages();
279

    
280
  // Load any pre-existing validation errors for the elements.
281
  $errors = array();
282

    
283
  foreach ((array) form_get_errors() as $element_path => $error) {
284
    list($pane_id, ) = explode('][', $element_path, 2);
285
    $errors[$pane_id][$element_path] = $error;
286
  }
287

    
288
  // Loop through the enabled checkout panes for the current page.
289
  $form_validate = TRUE;
290
  foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page['page_id'])) as $pane_id => $checkout_pane) {
291
    $validate = TRUE;
292

    
293
    // If any element in the pane failed validation, we mark the pane as
294
    // unvalidated and replay the validation messages on top of it.
295
    if (!empty($errors[$pane_id])) {
296
      $validate = FALSE;
297

    
298
      foreach ($errors[$pane_id] as $element_path => $message) {
299
        if ($message) {
300
          drupal_set_message($message, 'error');
301
        }
302
      }
303

    
304
      if (isset($previous_messages['error'])) {
305
        $previous_messages['error'] = array_values(array_diff($previous_messages['error'], $errors[$pane_id]));
306
      }
307
    }
308

    
309
    // If the pane has defined a checkout form validate handler...
310
    if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_validate')) {
311
      // Give it a chance to process the submitted data.
312
      $validate &= $callback($form, $form_state, $checkout_pane, $order);
313
    }
314

    
315
    // Catch and clear panes' messages.
316
    $pane_messages = drupal_get_messages();
317

    
318
    // Submit the pane if it validated.
319
    if ($validate && $callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_submit')) {
320
      $callback($form, $form_state, $checkout_pane, $order);
321
    }
322

    
323
    // Generate status messages.
324
    $form_state['storage']['messages'][$pane_id] = array_merge_recursive($pane_messages, drupal_get_messages());
325

    
326
    // A failed pane makes the form fail.
327
    $form_validate &= $validate;
328
  }
329

    
330
  // Restore messages and form errors.
331
  $_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
332
  $form_errors = &drupal_static('form_set_error', array());
333
  $form_state['storage']['errors'] = $form_errors;
334
  $form_errors = array();
335

    
336
  // Save the updated order object and reset the order in the form cache to
337
  // ensure rebuilt forms use the updated order.
338
  commerce_order_save($order);
339
  $form_state['build_info']['args'][0] = $order;
340

    
341
  // If a pane failed validation or the form state has otherwise been altered to
342
  // initiate a rebuild, return without moving to the next checkout page.
343
  if (!$form_validate || $form_state['rebuild']) {
344
    $form_state['rebuild'] = TRUE;
345
  }
346
}
347

    
348
/**
349
 * Submit handler for the continue button of the checkout form.
350
 */
351
function commerce_checkout_form_submit($form, &$form_state) {
352
  $checkout_page = $form_state['checkout_page'];
353

    
354
  // Load a fresh copy of the order stored in the form.
355
  $order = commerce_order_load($form_state['order']->order_id);
356

    
357
  // If we are going to redirect with checkout pane messages stored in the form
358
  // state, they will not be displayed on a subsequent form build like normal.
359
  // Move them out of the form state messages array and into the current
360
  // session's general message array instead.
361
  if (!empty($form_state['storage']['messages'])) {
362
    foreach ($form_state['storage']['messages'] as $pane_id => $pane_messages) {
363
      $_SESSION['messages'] = array_merge_recursive($_SESSION['messages'], $pane_messages);
364
    }
365
  }
366

    
367
  // If the form was submitted via the continue button...
368
  if (end($form_state['triggering_element']['#array_parents']) == 'continue') {
369
    // If there is another checkout page...
370
    if ($checkout_page['next_page']) {
371
      // Update the order status to reflect the next checkout page.
372
      $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.'));
373

    
374
      // If it happens to be the complete page, process completion now.
375
      if ($checkout_page['next_page'] == 'complete') {
376
        commerce_checkout_complete($order);
377
      }
378

    
379
      // Redirect to the next checkout page.
380
      $form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $checkout_page['next_page'];
381
    }
382
  }
383
}
384

    
385
/**
386
 * Special submit handler for the back button to avoid processing orders.
387
 */
388
function commerce_checkout_form_back_submit($form, &$form_state) {
389
  // If there is a previous page...
390
  if ($previous_page = commerce_checkout_page_load($form_state['checkout_page']['prev_page'])) {
391
    $order = commerce_order_load($form_state['order']->order_id);
392

    
393
    // Move the form back to that page.
394
    if ($previous_page['prev_page']) {
395
      $form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $previous_page['page_id'];
396
    }
397
    else {
398
      $form_state['redirect'] = 'checkout/' . $order->order_id;
399
    }
400

    
401
    // Update the order status for the checkout step.
402
    $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.'));
403
  }
404
}
405

    
406
/**
407
 * Special submit handler for the cancel button to avoid processing orders.
408
 */
409
function commerce_checkout_form_cancel_submit($form, &$form_state) {
410
  $order = commerce_order_load($form_state['order']->order_id);
411

    
412
  // Set the order status back to the first checkout page's status.
413
  $order_state = commerce_order_state_load('checkout');
414
  $form_state['order'] = commerce_order_status_update($order, $order_state['default_status'], TRUE);
415

    
416
  // Skip saving in the status update and manually save here to force a save
417
  // even when the status doesn't actually change.
418
  if (variable_get('commerce_order_auto_revision', TRUE)) {
419
    $form_state['order']->revision = TRUE;
420
    $form_state['order']->log = t('Customer manually canceled the checkout process.');
421
  }
422

    
423
  commerce_order_save($form_state['order']);
424

    
425
  drupal_set_message(t('Checkout of your current order has been canceled and may be resumed when you are ready.'));
426

    
427
  $form_state['redirect'] = $form_state['cancel_redirect'];
428
}
429

    
430
/**
431
 * Themes the optional checkout review page data.
432
 */
433
function theme_commerce_checkout_review($variables) {
434
  $form = $variables['form'];
435

    
436
  // Turn the review data array into table rows.
437
  $rows = array();
438

    
439
  foreach ($form['#data'] as $pane_id => $data) {
440
    // First add a row for the title.
441
    $rows[] = array(
442
      'data' => array(
443
        array('data' => $data['title'], 'colspan' => 2),
444
      ),
445
      'class' => array('pane-title', 'odd'),
446
    );
447

    
448
    // Next, add the data for this particular section.
449
    if (is_array($data['data'])) {
450
      // If it's an array, treat each key / value pair as a 2 column row.
451
      foreach ($data['data'] as $key => $value) {
452
        $rows[] = array(
453
          'data' => array(
454
            array('data' => $key .':', 'class' => array('pane-data-key')),
455
            array('data' => $value, 'class' => array('pane-data-value')),
456
          ),
457
          'class' => array('pane-data', 'even'),
458
        );
459
      }
460
    }
461
    else {
462
      // Otherwise treat it as a block of text in its own row.
463
      $rows[] = array(
464
        'data' => array(
465
          array('data' => $data['data'], 'colspan' => 2, 'class' => array('pane-data-full')),
466
        ),
467
        'class' => array('pane-data', 'even'),
468
      );
469
    }
470
  }
471

    
472
  return theme('table', array('rows' => $rows, 'attributes' => array('class' => array('checkout-review'))));
473
}