Projet

Général

Profil

Paste
Télécharger (106 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / commerce / modules / cart / commerce_cart.module @ dbb0c974

1
<?php
2

    
3
/**
4
 * @file
5
 * Implements the shopping cart system and add to cart features.
6
 *
7
 * In Drupal Commerce, the shopping cart is really just an order that makes
8
 * special considerations to associate it with a user and
9
 */
10

    
11
// Define constants for the shopping cart refresh modes.
12
define('COMMERCE_CART_REFRESH_ALWAYS', 'always');
13
define('COMMERCE_CART_REFRESH_OWNER_ONLY', 'owner_only');
14
define('COMMERCE_CART_REFRESH_ACTIVE_CART_ONLY', 'active_cart_only');
15
define('COMMERCE_CART_REFRESH_DEFAULT_FREQUENCY', 15);
16

    
17
/**
18
 * Implements hook_menu().
19
 */
20
function commerce_cart_menu() {
21
  $items = array();
22

    
23
  $items['cart'] = array(
24
    'title' => 'Shopping cart',
25
    'page callback' => 'commerce_cart_view',
26
    'access arguments' => array('access content'),
27
    'file' => 'includes/commerce_cart.pages.inc',
28
  );
29

    
30
  $items['cart/my'] = array(
31
    'title' => 'Shopping cart (# items)',
32
    'title callback' => 'commerce_cart_menu_item_title',
33
    'title arguments' => array(TRUE),
34
    'page callback' => 'commerce_cart_menu_item_redirect',
35
    'access arguments' => array('access content'),
36
    'type' => MENU_SUGGESTED_ITEM,
37
  );
38

    
39
  $items['checkout'] = array(
40
    'title' => 'Checkout',
41
    'page callback' => 'commerce_cart_checkout_router',
42
    'access arguments' => array('access checkout'),
43
    'type' => MENU_CALLBACK,
44
    'file' => 'includes/commerce_cart.pages.inc',
45
  );
46

    
47
  // If the Order UI module is installed, add a local action to it that lets an
48
  // administrator execute a cart order refresh on the order. Modules that
49
  // define their own order edit menu item are also responsible for defining
50
  // their own local action menu items if needed.
51
  if (module_exists('commerce_order_ui')) {
52
    $items['admin/commerce/orders/%commerce_order/edit/refresh'] = array(
53
      'title' => 'Apply pricing rules',
54
      'description' => 'Executes the cart order refresh used to apply all current pricing rules on the front end.',
55
      'page callback' => 'drupal_get_form',
56
      'page arguments' => array('commerce_cart_order_refresh_form', 3),
57
      'access callback' => 'commerce_cart_order_refresh_form_access',
58
      'access arguments' => array(3),
59
      'type' => MENU_LOCAL_ACTION,
60
      'file' => 'includes/commerce_cart.admin.inc',
61
    );
62
  }
63

    
64
  return $items;
65
}
66

    
67
/**
68
 * Returns the title of the shopping cart menu item with an item count.
69
 */
70
function commerce_cart_menu_item_title() {
71
  global $user;
72

    
73
  // Default to a static title.
74
  $title = t('Shopping cart');
75

    
76
  // If the user actually has a cart order...
77
  if ($order = commerce_cart_order_load($user->uid)) {
78
    // Count the number of product line items on the order.
79
    $wrapper = entity_metadata_wrapper('commerce_order', $order);
80
    $quantity = commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types());
81

    
82
    // If there are more than 0 product line items on the order...
83
    if ($quantity > 0) {
84
      // Use the dynamic menu item title.
85
      $title = format_plural($quantity, 'Shopping cart (1 item)', 'Shopping cart (@count items)');
86
    }
87
  }
88

    
89
  return $title;
90
}
91

    
92
/**
93
 * Redirects a valid page request to cart/my to the cart page.
94
 */
95
function commerce_cart_menu_item_redirect() {
96
  drupal_goto('cart');
97
}
98

    
99
/**
100
 * Access callback: determines access to the "Apply pricing rules" local action.
101
 */
102
function commerce_cart_order_refresh_form_access($order) {
103
  // Do not show the link for cart orders as they're refreshed automatically.
104
  if (commerce_cart_order_is_cart($order)) {
105
    return FALSE;
106
  }
107

    
108
  // Returns TRUE if the link is enabled via the order settings form and the
109
  // user has access to update the order.
110
  return variable_get('commerce_order_apply_pricing_rules_link', TRUE) && commerce_order_access('update', $order);
111
}
112

    
113
/**
114
 * Implements hook_hook_info().
115
 */
116
function commerce_cart_hook_info() {
117
  $hooks = array(
118
    'commerce_cart_order_id' => array(
119
      'group' => 'commerce',
120
    ),
121
    'commerce_cart_order_is_cart' => array(
122
      'group' => 'commerce',
123
    ),
124
    'commerce_cart_order_convert' => array(
125
      'group' => 'commerce',
126
    ),
127
    'commerce_cart_line_item_refresh' => array(
128
      'group' => 'commerce',
129
    ),
130
    'commerce_cart_order_refresh' => array(
131
      'group' => 'commerce',
132
    ),
133
    'commerce_cart_order_empty' => array(
134
      'group' => 'commerce',
135
    ),
136
    'commerce_cart_attributes_refresh_alter' => array(
137
      'group' => 'commerce',
138
    ),
139
    'commerce_cart_product_comparison_properties_alter' => array(
140
      'group' => 'commerce',
141
    ),
142
    'commerce_cart_product_prepare' => array(
143
      'group' => 'commerce',
144
    ),
145
    'commerce_cart_product_add' => array(
146
      'group' => 'commerce',
147
    ),
148
    'commerce_cart_product_remove' => array(
149
      'group' => 'commerce',
150
    ),
151
  );
152

    
153
  return $hooks;
154
}
155

    
156
/**
157
 * Implements hook_commerce_order_state_info().
158
 */
159
function commerce_cart_commerce_order_state_info() {
160
  $order_states = array();
161

    
162
  $order_states['cart'] = array(
163
    'name' => 'cart',
164
    'title' => t('Shopping cart'),
165
    'description' => t('Orders in this state have not been completed by the customer yet.'),
166
    'weight' => -5,
167
    'default_status' => 'cart',
168
  );
169

    
170
  return $order_states;
171
}
172

    
173
/**
174
 * Implements hook_commerce_order_status_info().
175
 */
176
function commerce_cart_commerce_order_status_info() {
177
  $order_statuses = array();
178

    
179
  $order_statuses['cart'] = array(
180
    'name' => 'cart',
181
    'title' => t('Shopping cart'),
182
    'state' => 'cart',
183
    'cart' => TRUE,
184
  );
185

    
186
  return $order_statuses;
187
}
188

    
189
/**
190
 * Implements hook_commerce_checkout_pane_info().
191
 */
192
function commerce_cart_commerce_checkout_pane_info() {
193
  $checkout_panes = array();
194

    
195
  $checkout_panes['cart_contents'] = array(
196
    'title' => t('Shopping cart contents'),
197
    'base' => 'commerce_cart_contents_pane',
198
    'file' => 'includes/commerce_cart.checkout_pane.inc',
199
    'page' => 'checkout',
200
    'weight' => -10,
201
  );
202

    
203
  return $checkout_panes;
204
}
205

    
206
/**
207
 * Implements hook_commerce_checkout_complete().
208
 */
209
function commerce_cart_commerce_checkout_complete($order) {
210
  // Move the cart order ID to a completed order ID.
211
  if (commerce_cart_order_session_exists($order->order_id)) {
212
    commerce_cart_order_session_save($order->order_id, TRUE);
213
    commerce_cart_order_session_delete($order->order_id);
214
  }
215
}
216

    
217
/**
218
 * Implements hook_commerce_line_item_summary_link_info().
219
 */
220
function commerce_cart_commerce_line_item_summary_link_info() {
221
  return array(
222
    'view_cart' => array(
223
      'title' => t('View cart'),
224
      'href' => 'cart',
225
      'attributes' => array('rel' => 'nofollow'),
226
      'weight' => 0,
227
    ),
228
    'checkout' => array(
229
      'title' => t('Checkout'),
230
      'href' => 'checkout',
231
      'attributes' => array('rel' => 'nofollow'),
232
      'weight' => 5,
233
      'access' => user_access('access checkout'),
234
    ),
235
  );
236
}
237

    
238
/**
239
 * Implements hook_form_alter().
240
 */
241
function commerce_cart_form_alter(&$form, &$form_state, $form_id) {
242
  if (strpos($form_id, 'views_form_commerce_cart_form_') === 0) {
243
    // Only alter buttons if the cart form View shows line items.
244
    $view = reset($form_state['build_info']['args']);
245

    
246
    if (!empty($view->result)) {
247
      // Change the Save button to say Update cart.
248
      $form['actions']['submit']['#value'] = t('Update cart');
249
      $form['actions']['submit']['#submit'] = array_merge($form['#submit'], array('commerce_cart_line_item_views_form_submit'));
250

    
251
      // Change any Delete buttons to say Remove.
252
      if (!empty($form['edit_delete'])) {
253
        foreach(element_children($form['edit_delete']) as $key) {
254
          // Load and wrap the line item to have the title in the submit phase.
255
          if (!empty($form['edit_delete'][$key]['#line_item_id'])) {
256
            $line_item_id = $form['edit_delete'][$key]['#line_item_id'];
257
            $form_state['line_items'][$line_item_id] = commerce_line_item_load($line_item_id);
258

    
259
            $form['edit_delete'][$key]['#value'] = t('Remove');
260
            $form['edit_delete'][$key]['#submit'] = array_merge($form['#submit'], array('commerce_cart_line_item_delete_form_submit'));
261
          }
262
        }
263
      }
264
    }
265
    else {
266
      // Otherwise go ahead and remove any buttons from the View.
267
      unset($form['actions']);
268
    }
269
  }
270
  elseif (strpos($form_id, 'commerce_checkout_form_') === 0 && !empty($form['buttons']['cancel'])) {
271
    // Override the submit handler for changing the order status on checkout cancel.
272
    foreach ($form['buttons']['cancel']['#submit'] as $key => &$value) {
273
      if ($value == 'commerce_checkout_form_cancel_submit') {
274
        $value = 'commerce_cart_checkout_form_cancel_submit';
275
      }
276
    }
277
  }
278
  elseif (strpos($form_id, 'views_form_commerce_cart_block') === 0) {
279
    // No point in having a "Save" button on the shopping cart block.
280
    unset($form['actions']);
281
  }
282
}
283

    
284
/**
285
 * Submit handler to take back the order to cart status on cancel in checkout.
286
 */
287
function commerce_cart_checkout_form_cancel_submit($form, &$form_state) {
288
  // Update the order to the cart status.
289
  $order = commerce_order_load($form_state['order']->order_id);
290
  $form_state['order'] = commerce_order_status_update($order, 'cart', TRUE);
291

    
292
  // Skip saving in the status update and manually save here to force a save
293
  // even when the status doesn't actually change.
294
  if (variable_get('commerce_order_auto_revision', TRUE)) {
295
    $form_state['order']->revision = TRUE;
296
    $form_state['order']->log = t('Customer manually canceled the checkout process.');
297
  }
298

    
299
  commerce_order_save($form_state['order']);
300

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

    
303
  // Redirect to cart on cancel.
304
  $form_state['redirect'] = 'cart';
305
}
306

    
307
/**
308
 * Submit handler to show the shopping cart updated message.
309
 */
310
function commerce_cart_line_item_views_form_submit($form, &$form_state) {
311
  // Reset the status of the order to cart.
312
  $order = commerce_order_load($form_state['order']->order_id);
313
  $form_state['order'] = commerce_order_status_update($order, 'cart', TRUE);
314

    
315
  // Skip saving in the status update and manually save here to force a save
316
  // even when the status doesn't actually change.
317
  if (variable_get('commerce_order_auto_revision', TRUE)) {
318
    $form_state['order']->revision = TRUE;
319
    $form_state['order']->log = t('Customer updated the order via the shopping cart form.');
320
  }
321

    
322
  commerce_order_save($form_state['order']);
323

    
324
  drupal_set_message(t('Your shopping cart has been updated.'));
325
}
326

    
327
/**
328
 * Submit handler to show the line item delete message.
329
 */
330
function commerce_cart_line_item_delete_form_submit($form, &$form_state) {
331
  $line_item_id = $form_state['triggering_element']['#line_item_id'];
332

    
333
  // Get the corresponding wrapper to show the correct title.
334
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $form_state['line_items'][$line_item_id]);
335

    
336
  // If the deleted line item is a product...
337
  if (in_array($line_item_wrapper->type->value(), commerce_product_line_item_types())) {
338
    $title = $line_item_wrapper->commerce_product->title->value();
339
  }
340
  else {
341
    $title = $line_item_wrapper->line_item_label->value();
342
  }
343

    
344
  drupal_set_message(t('%title removed from your cart.', array('%title' => $title)));
345
}
346

    
347
/**
348
 * Implements hook_form_FORM_ID_alter().
349
 *
350
 * Adds a checkbox to the order settings form to enable the local action on
351
 * order edit forms to apply pricing rules.
352
 */
353
function commerce_cart_form_commerce_order_settings_form_alter(&$form, &$form_state) {
354
  $form['commerce_order_apply_pricing_rules_link'] = array(
355
    '#type' => 'checkbox',
356
    '#title' => t('Enable the local action link on order edit forms to apply pricing rules.'),
357
    '#description' => t('Even if enabled the link will not appear on shopping cart order edit forms.'),
358
    '#default_value' => variable_get('commerce_order_apply_pricing_rules_link', TRUE),
359
    '#weight' => 10,
360
  );
361

    
362
  // Add a fieldset for settings pertaining to the shopping cart refresh.
363
  $form['cart_refresh'] = array(
364
    '#type' => 'fieldset',
365
    '#title' => t('Shopping cart refresh'),
366
    '#description' => t('Shopping cart orders comprise orders in shopping cart and some checkout related order statuses. These settings let you control the shopping cart orders are refreshed, the process during which product prices are recalculated, to improve site performance in the case of excessive refreshes on sites with less dynamic pricing needs.'),
367
    '#weight' => 40,
368
  );
369
  $form['cart_refresh']['commerce_cart_refresh_mode'] = array(
370
    '#type' => 'radios',
371
    '#title' => t('Shopping cart refresh mode'),
372
    '#options' => array(
373
      COMMERCE_CART_REFRESH_ALWAYS => t('Refresh a shopping cart when it is loaded regardless of who it belongs to.'),
374
      COMMERCE_CART_REFRESH_OWNER_ONLY => t('Only refresh a shopping cart when it is loaded if it belongs to the current user.'),
375
      COMMERCE_CART_REFRESH_ACTIVE_CART_ONLY => t("Only refresh a shopping cart when it is loaded if it is the current user's active shopping cart."),
376
    ),
377
    '#default_value' => variable_get('commerce_cart_refresh_mode', COMMERCE_CART_REFRESH_OWNER_ONLY),
378
  );
379
  $form['cart_refresh']['commerce_cart_refresh_frequency'] = array(
380
    '#type' => 'textfield',
381
    '#title' => t('Shopping cart refresh frequency'),
382
    '#description' => t('Shopping carts will only be refreshed if more than the specified number of seconds have passed since they were last refreshed.'),
383
    '#default_value' => variable_get('commerce_cart_refresh_frequency', COMMERCE_CART_REFRESH_DEFAULT_FREQUENCY),
384
    '#required' => TRUE,
385
    '#size' => 32,
386
    '#field_suffix' => t('seconds'),
387
    '#element_validate' => array('commerce_cart_validate_refresh_frequency'),
388
  );
389
  $form['cart_refresh']['commerce_cart_refresh_force'] = array(
390
    '#type' => 'checkbox',
391
    '#title' => t('Always refresh shopping cart orders on shopping cart and checkout form pages regardless of other settings.'),
392
    '#description' => t('Note: this option only applies to the core /cart and /checkout/* paths.'),
393
    '#default_value' => variable_get('commerce_cart_refresh_force', TRUE),
394
  );
395
}
396

    
397
/**
398
 * Form element validation handler for the cart refresh frequency value.
399
 */
400
function commerce_cart_validate_refresh_frequency($element, &$form_state) {
401
  $value = $element['#value'];
402
  if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value < 0)) {
403
    form_error($element, t('%name must be 0 or a positive integer.', array('%name' => $element['#title'])));
404
  }
405
}
406

    
407
/**
408
 * Implements hook_form_FORM_ID_alter().
409
 *
410
 * Alter the order edit form so administrators cannot attempt to alter line item
411
 * unit prices for orders still in a shopping cart status. On order load, the
412
 * cart module refreshes these prices based on the current product price and
413
 * pricing rules, so any alterations would not be persistent anyways.
414
 *
415
 * @see commerce_cart_commerce_order_load()
416
 */
417
function commerce_cart_form_commerce_order_ui_order_form_alter(&$form, &$form_state) {
418
  $order = $form_state['commerce_order'];
419

    
420
  // If the order being edited is in a shopping cart status and the form has the
421
  // commerce_line_items element present...
422
  if (commerce_cart_order_is_cart($order) && !empty($form['commerce_line_items'])) {
423
    // Grab the instance info for commerce_line_items and only alter the form if
424
    // it's using the line item manager widget.
425
    $instance = field_info_instance('commerce_order', 'commerce_line_items', field_extract_bundle('commerce_order', $order));
426

    
427
    if ($instance['widget']['type'] == 'commerce_line_item_manager') {
428
      // Loop over the line items on the form...
429
      foreach ($form['commerce_line_items'][$form['commerce_line_items']['#language']]['line_items'] as &$line_item) {
430
        // Disable the unit price amount and currency code fields.
431
        $language = $line_item['commerce_unit_price']['#language'];
432
        $line_item['commerce_unit_price'][$language][0]['amount']['#disabled'] = TRUE;
433
        $line_item['commerce_unit_price'][$language][0]['currency_code']['#disabled'] = TRUE;
434
      }
435
    }
436
  }
437
}
438

    
439
/**
440
 * Implements hook_form_FORM_ID_alter().
441
 *
442
 * Alters the Field UI field edit form to add per-instance settings for fields
443
 * on product types governing the use of product fields as attribute selection
444
 * fields on the Add to Cart form.
445
 */
446
function commerce_cart_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
447
  // Extract the instance info from the form.
448
  $instance = $form['#instance'];
449

    
450
  // If the current field instance is not locked, is attached to a product type,
451
  // and of a field type that defines an options list...
452
  if (empty($form['locked']) && $instance['entity_type'] == 'commerce_product' &&
453
    function_exists($form['#field']['module'] . '_options_list')) {
454
    // Get the current instance's attribute settings for use as default values.
455
    $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
456

    
457
    $form['instance']['commerce_cart_settings'] = array(
458
      '#type' => 'fieldset',
459
      '#title' => t('Attribute field settings'),
460
      '#description' => t('Single value fields attached to products can function as attribute selection fields on Add to Cart forms. When an Add to Cart form contains multiple products, attribute field data can be used to allow customers to select a product based on the values of the field instead of just from a list of product titles.'),
461
      '#weight' => 5,
462
      '#collapsible' => FALSE,
463
    );
464
    $form['instance']['commerce_cart_settings']['attribute_field'] = array(
465
      '#type' => 'checkbox',
466
      '#title' => t('Enable this field to function as an attribute field on Add to Cart forms.'),
467
      '#default_value' => $commerce_cart_settings['attribute_field'],
468
    );
469
    $form['instance']['commerce_cart_settings']['attribute_widget'] = array(
470
      '#type' => 'radios',
471
      '#title' => t('Attribute selection widget'),
472
      '#description' => t('The type of element used to select an option if used on an Add to Cart form.'),
473
      '#options' => array(
474
        'select' => t('Select list'),
475
        'radios' => t('Radio buttons'),
476
      ),
477
      '#default_value' => $commerce_cart_settings['attribute_widget'],
478
      '#states' => array(
479
        'visible' => array(
480
          ':input[name="instance[commerce_cart_settings][attribute_field]"]' => array('checked' => TRUE),
481
        ),
482
      ),
483
    );
484

    
485
    // Determine the default attribute widget title.
486
    $attribute_widget_title = $commerce_cart_settings['attribute_widget_title'];
487

    
488
    if (empty($attribute_widget_title)) {
489
      $attribute_widget_title = $instance['label'];
490
    }
491

    
492
    $form['instance']['commerce_cart_settings']['attribute_widget_title'] = array(
493
      '#type' => 'textfield',
494
      '#title' => t('Attribute widget title'),
495
      '#description' => t('Specify the title to use for the attribute widget on the Add to Cart form.'),
496
      '#default_value' => $attribute_widget_title,
497
      '#states' => array(
498
        'visible' => array(
499
          ':input[name="instance[commerce_cart_settings][attribute_field]"]' => array('checked' => TRUE),
500
        ),
501
      ),
502
    );
503

    
504
    $form['field']['cardinality']['#description'] .= '<br />' . t('Must be 1 for this field to function as an attribute selection field on Add to Cart forms.');
505
  }
506

    
507
  // If the current field instance is not locked and is attached to a product
508
  // line item type...
509
  if (empty($form['locked']) && $instance['entity_type'] == 'commerce_line_item' &&
510
    in_array($instance['bundle'], commerce_product_line_item_types())) {
511
    // Get the current instance's line item form settings for use as default values.
512
    $commerce_cart_settings = commerce_cart_field_instance_access_settings($instance);
513

    
514
    $form['instance']['commerce_cart_settings'] = array(
515
      '#type' => 'fieldset',
516
      '#title' => t('Add to Cart form settings'),
517
      '#description' =>t('Fields attached to product line item types can be included in the Add to Cart form to collect additional information from customers in conjunction with their purchase of particular products.'),
518
      '#weight' => 5,
519
      '#collapsible' => FALSE,
520
    );
521
    $form['instance']['commerce_cart_settings']['field_access'] = array(
522
      '#type' => 'checkbox',
523
      '#title' => t('Include this field on Add to Cart forms for line items of this type.'),
524
      '#default_value' => $commerce_cart_settings['field_access'],
525
    );
526
  }
527
}
528

    
529
/**
530
 * Implements hook_commerce_order_delete().
531
 */
532
function commerce_cart_commerce_order_delete($order) {
533
  commerce_cart_order_session_delete($order->order_id);
534
  commerce_cart_order_session_delete($order->order_id, TRUE);
535
}
536

    
537
/**
538
 * Implements hook_commerce_product_calculate_sell_price_line_item_alter().
539
 */
540
function commerce_cart_commerce_product_calculate_sell_price_line_item_alter($line_item) {
541
  global $user;
542

    
543
  // Reference the current shopping cart order in the line item if it isn't set.
544
  // We load the complete order at this time to ensure it primes the order cache
545
  // and avoid any untraceable recursive loops.
546
  // @see http://drupal.org/node/1268472
547
  if (empty($line_item->order_id)) {
548
    $order = commerce_cart_order_load($user->uid);
549

    
550
    if ($order) {
551
      $line_item->order_id = $order->order_id;
552
    }
553
  }
554
}
555

    
556
/**
557
 * Implements hook_views_api().
558
 */
559
function commerce_cart_views_api() {
560
  return array(
561
    'api' => 3,
562
    'path' => drupal_get_path('module', 'commerce_cart') . '/includes/views',
563
  );
564
}
565

    
566
/**
567
 * Implements hook_theme().
568
 */
569
function commerce_cart_theme() {
570
  return array(
571
    'commerce_cart_empty_block' => array(
572
      'variables' => array(),
573
    ),
574
    'commerce_cart_empty_page' => array(
575
      'variables' => array(),
576
    ),
577
    'commerce_cart_block' => array(
578
      'variables' => array('order' => NULL, 'contents_view' => NULL),
579
      'path' => drupal_get_path('module', 'commerce_cart') . '/theme',
580
      'template' => 'commerce-cart-block',
581
    ),
582
  );
583
}
584

    
585
/**
586
 * Implements hook_user_login().
587
 *
588
 * When a user logs into the site, if they have a shopping cart order it should
589
 * be updated to belong to their user account.
590
 */
591
function commerce_cart_user_login(&$edit, $account) {
592
  // Get the user's anonymous shopping cart order if it exists.
593
  if ($order = commerce_cart_order_load()) {
594
    // Convert it to an authenticated cart.
595
    commerce_cart_order_convert($order, $account);
596
  }
597
}
598

    
599
/**
600
 * Implements hook_user_update().
601
 *
602
 * When a user account e-mail address is updated, update any shopping cart
603
 * orders owned by the user account to use the new e-mail address.
604
 */
605
function commerce_cart_user_update(&$edit, $account, $category) {
606
  // If the e-mail address was changed...
607
  if (!empty($edit['original']->mail) && $account->mail != $edit['original']->mail) {
608
    // Load the user's shopping cart orders.
609
    $query = new EntityFieldQuery();
610

    
611
    $query
612
      ->entityCondition('entity_type', 'commerce_order', '=')
613
      ->propertyCondition('uid', $account->uid, '=')
614
      ->propertyCondition('status', array_keys(commerce_order_statuses(array('cart' => TRUE))), 'IN');
615

    
616
    $result = $query->execute();
617

    
618
    if (!empty($result['commerce_order'])) {
619
      foreach (commerce_order_load_multiple(array_keys($result['commerce_order'])) as $order) {
620
        if ($order->mail != $account->mail) {
621
          $order->mail = $account->mail;
622
          commerce_order_save($order);
623
        }
624
      }
625
    }
626
  }
627
}
628

    
629
/**
630
 * Implements hook_block_info().
631
 */
632
function commerce_cart_block_info() {
633
  $blocks = array();
634

    
635
  // Define the basic shopping cart block and hide it on the checkout pages.
636
  $blocks['cart'] = array(
637
    'info' => t('Shopping cart'),
638
    'cache' => DRUPAL_NO_CACHE,
639
    'visibility' => 0,
640
    'pages' => 'checkout*',
641
  );
642

    
643
  return $blocks;
644
}
645

    
646
/**
647
 * Implements hook_block_view().
648
 */
649
function commerce_cart_block_view($delta) {
650
  global $user;
651

    
652
  // Prepare the display of the default Shopping Cart block.
653
  if ($delta == 'cart') {
654
    // Default to an empty cart block message.
655
    $content = theme('commerce_cart_empty_block');
656

    
657
    // First check to ensure there are products in the shopping cart.
658
    if ($order = commerce_cart_order_load($user->uid)) {
659
      $wrapper = entity_metadata_wrapper('commerce_order', $order);
660

    
661
      // If there are one or more products in the cart...
662
      if (commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types()) > 0) {
663

    
664
        // Build the variables array to send to the cart block template.
665
        $variables = array(
666
          'order' => $order,
667
          'contents_view' => commerce_embed_view('commerce_cart_block', 'default', array($order->order_id), $_GET['q']),
668
        );
669

    
670
        $content = theme('commerce_cart_block', $variables);
671
      }
672
    }
673

    
674
    return array('subject' => t('Shopping cart'), 'content' => $content);
675
  }
676
}
677

    
678
 /**
679
 * Checks if a cart order should be refreshed based on the shopping cart refresh
680
 * settings on the order settings form.
681
 *
682
 * @param $order
683
 *   The cart order to check.
684
 *
685
 * @return
686
 *   Boolean indicating whether or not the cart order can be refreshed.
687
 */
688
function commerce_cart_order_can_refresh($order) {
689
  global $user;
690

    
691
  // Force the shopping cart refresh on /cart and /checkout/* paths if enabled.
692
  if (variable_get('commerce_cart_refresh_force', TRUE) &&
693
    (current_path() == 'cart' || strpos(current_path(), 'checkout/') === 0)) {
694
    return TRUE;
695
  }
696

    
697
  // Prevent refresh for orders that don't match the current refresh mode.
698
  switch (variable_get('commerce_cart_refresh_mode', COMMERCE_CART_REFRESH_OWNER_ONLY)) {
699
    case COMMERCE_CART_REFRESH_OWNER_ONLY:
700
      // If the order is anonymous, check the session to see if the order
701
      // belongs to the current user. Otherwise just check that the order uid
702
      // matches the current user.
703
      if ($order->uid == 0 && !commerce_cart_order_session_exists($order->order_id)) {
704
        return FALSE;
705
      }
706
      elseif ($order->uid != $user->uid) {
707
        return FALSE;
708
      }
709
      break;
710

    
711
    case COMMERCE_CART_REFRESH_ACTIVE_CART_ONLY:
712
      // Check to see if the order ID matches the current user's cart order ID.
713
      if (commerce_cart_order_id($user->uid) != $order->order_id) {
714
        return FALSE;
715
      }
716
      break;
717

    
718
    case COMMERCE_CART_REFRESH_ALWAYS:
719
    default:
720
      // Continue on if shopping cart orders should always refresh.
721
      break;
722
  }
723

    
724
  // Check to see if the last cart refresh happened long enough ago.
725
  $seconds = variable_get('commerce_cart_refresh_frequency', COMMERCE_CART_REFRESH_DEFAULT_FREQUENCY);
726

    
727
  if (!empty($seconds) && !empty($order->data['last_cart_refresh']) &&
728
    REQUEST_TIME - $order->data['last_cart_refresh'] < $seconds) {
729
    return FALSE;
730
  }
731

    
732
  return TRUE;
733
}
734

    
735
/**
736
 * Implements hook_commerce_order_load().
737
 *
738
 * Because shopping carts are merely a special case of orders, we work through
739
 * the Order API to ensure that products in shopping carts are kept up to date.
740
 * Therefore, each time a cart is loaded, we calculate afresh the unit and total
741
 * prices of product line items and save them if any values have changed.
742
 */
743
function commerce_cart_commerce_order_load($orders) {
744
  $refreshed = &drupal_static(__FUNCTION__, array());
745

    
746
  foreach ($orders as $order) {
747
    // Refresh only if this order object represents the latest revision of a
748
    // shopping cart order, it hasn't been refreshed already in this request
749
    // and it meets the criteria in the shopping cart refresh settings.
750
    if (!isset($refreshed[$order->order_id]) &&
751
      commerce_cart_order_is_cart($order) &&
752
      commerce_order_is_latest_revision($order) &&
753
      commerce_cart_order_can_refresh($order)) {
754
      // Update the last cart refresh timestamp and record the order's current
755
      // changed timestamp to detect if the order is actually updated.
756
      $order->data['last_cart_refresh'] = REQUEST_TIME;
757

    
758
      $unchanged_data = $order->data;
759
      $last_changed = $order->changed;
760

    
761
      // Refresh the order and add its ID to the refreshed array.
762
      $refreshed[$order->order_id] = TRUE;
763
      commerce_cart_order_refresh($order);
764

    
765
      // If order wasn't updated during the refresh, we need to manually update
766
      // the last cart refresh timestamp in the database.
767
      if ($order->changed == $last_changed) {
768
        db_update('commerce_order')
769
          ->fields(array('data' => serialize($unchanged_data)))
770
          ->condition('order_id', $order->order_id)
771
          ->execute();
772

    
773
        db_update('commerce_order_revision')
774
          ->fields(array('data' => serialize($unchanged_data)))
775
          ->condition('order_id', $order->order_id)
776
          ->condition('revision_id', $order->revision_id)
777
          ->execute();
778
      }
779
    }
780
  }
781
}
782

    
783
/**
784
 * Themes an empty shopping cart block's contents.
785
 */
786
function theme_commerce_cart_empty_block() {
787
  return '<div class="cart-empty-block">' . t('Your shopping cart is empty.') . '</div>';
788
}
789

    
790
/**
791
 * Themes an empty shopping cart page.
792
 */
793
function theme_commerce_cart_empty_page() {
794
  return '<div class="cart-empty-page">' . t('Your shopping cart is empty.') . '</div>';
795
}
796

    
797
/**
798
 * Loads the shopping cart order for the specified user.
799
 *
800
 * @param $uid
801
 *   The uid of the customer whose cart to load. If left 0, attempts to load
802
 *   an anonymous order from the session.
803
 *
804
 * @return
805
 *   The fully loaded shopping cart order or FALSE if nonexistent.
806
 */
807
function commerce_cart_order_load($uid = 0) {
808
  // Retrieve the order ID for the specified user's current shopping cart.
809
  $order_id = commerce_cart_order_id($uid);
810

    
811
  // If a valid cart order ID exists for the user, return it now.
812
  if (!empty($order_id)) {
813
    return commerce_order_load($order_id);
814
  }
815

    
816
  return FALSE;
817
}
818

    
819
/**
820
 * Returns the current cart order ID for the given user.
821
 *
822
 * @param $uid
823
 *   The uid of the customer whose cart to load. If left 0, attempts to load
824
 *   an anonymous order from the session.
825
 *
826
 * @return
827
 *   The requested cart order ID or FALSE if none was found.
828
 */
829
function commerce_cart_order_id($uid = 0) {
830
  // Cart order IDs will be cached keyed by $uid.
831
  $cart_order_ids = &drupal_static(__FUNCTION__);
832

    
833
  // Cache the user's cart order ID if it hasn't been set already.
834
  if (isset($cart_order_ids[$uid])) {
835
    return $cart_order_ids[$uid];
836
  }
837

    
838
  // First let other modules attempt to provide a valid order ID for the given
839
  // uid. Instead of invoking hook_commerce_cart_order_id() directly, we invoke
840
  // it in each module implementing the hook and return the first valid order ID
841
  // returned (if any).
842
  foreach (module_implements('commerce_cart_order_id') as $module) {
843
    $order_id = module_invoke($module, 'commerce_cart_order_id', $uid);
844

    
845
    // If a hook said the user should not have a cart, that overrides any other
846
    // potentially valid order ID. Return FALSE now.
847
    if ($order_id === FALSE) {
848
      $cart_order_ids[$uid] = FALSE;
849
      return FALSE;
850
    }
851

    
852
    // Otherwise only return a valid order ID.
853
    if (!empty($order_id) && is_int($order_id)) {
854
      $cart_order_ids[$uid] = $order_id;
855
      return $order_id;
856
    }
857
  }
858

    
859
  // Create an array of valid shopping cart order statuses.
860
  $status_ids = array_keys(commerce_order_statuses(array('cart' => TRUE)));
861

    
862
  // If a customer uid was specified...
863
  if ($uid) {
864
    // Look for the user's most recent shopping cart order, although they
865
    // should never really have more than one.
866
    $cart_order_ids[$uid] = db_query('SELECT order_id FROM {commerce_order} WHERE uid = :uid AND status IN (:status_ids) ORDER BY order_id DESC', array(':uid' => $uid, ':status_ids' => $status_ids))->fetchField();
867
  }
868
  else {
869
    // Otherwise look for a shopping cart order ID in the session.
870
    if (commerce_cart_order_session_exists()) {
871
      // We can't trust a user's IP address to remain the same, especially since
872
      // it may be derived from a proxy server and not the actual client. As of
873
      // Commerce 1.4, this query no longer restricts order IDs based on IP
874
      // address, instead trusting Drupal to prevent session hijacking.
875
      $cart_order_ids[$uid] = db_query('SELECT order_id FROM {commerce_order} WHERE order_id IN (:order_ids) AND uid = 0 AND status IN (:status_ids) ORDER BY order_id DESC', array(':order_ids' => commerce_cart_order_session_order_ids(), ':status_ids' => $status_ids))->fetchField();
876
    }
877
    else {
878
      $cart_order_ids[$uid] = FALSE;
879
    }
880
  }
881

    
882
  return $cart_order_ids[$uid];
883
}
884

    
885
/**
886
 * Resets the cached array of shopping cart orders.
887
 */
888
function commerce_cart_order_ids_reset() {
889
  $cart_order_ids = &drupal_static('commerce_cart_order_id');
890
  $cart_order_ids = NULL;
891
}
892

    
893
/**
894
 * Creates a new shopping cart order for the specified user.
895
 *
896
 * @param $uid
897
 *   The uid of the user for whom to create the order. If left 0, the order will
898
 *   be created for an anonymous user and associated with the current session
899
 *   if it is anonymous.
900
 * @param $type
901
 *   The type of the order; defaults to the standard 'commerce_order' type.
902
 *
903
 * @return
904
 *   The newly created shopping cart order object.
905
 */
906
function commerce_cart_order_new($uid = 0, $type = 'commerce_order') {
907
  global $user;
908

    
909
  // Create the new order with the customer's uid and the cart order status.
910
  $order = commerce_order_new($uid, 'cart', $type);
911
  $order->log = t('Created as a shopping cart order.');
912

    
913
  // Save it so it gets an order ID and return the full object.
914
  commerce_order_save($order);
915

    
916
  // Reset the cart cache
917
  commerce_cart_order_ids_reset();
918

    
919
  // If the user is not logged in, ensure the order ID is stored in the session.
920
  if (!$uid && empty($user->uid)) {
921
    commerce_cart_order_session_save($order->order_id);
922
  }
923

    
924
  return $order;
925
}
926

    
927
/**
928
 * Determines whether or not the given order is a shopping cart order.
929
 */
930
function commerce_cart_order_is_cart($order) {
931
  // If the order is in a shopping cart order status, assume it is a cart.
932
  $is_cart = in_array($order->status, array_keys(commerce_order_statuses(array('cart' => TRUE))));
933

    
934
  // Allow other modules to make the judgment based on some other criteria.
935
  foreach (module_implements('commerce_cart_order_is_cart') as $module) {
936
    $function = $module . '_commerce_cart_order_is_cart';
937

    
938
    // As of Drupal Commerce 1.2, $is_cart should be accepted by reference and
939
    // manipulated directly, but we still check for a return value to preserve
940
    // backward compatibility with the hook. In future versions, we will
941
    // deprecate hook_commerce_cart_order_is_cart() and force modules to update
942
    // to hook_commerce_cart_order_is_cart_alter().
943
    if ($function($order, $is_cart) === FALSE) {
944
      $is_cart = FALSE;
945
    }
946
  }
947

    
948
  drupal_alter('commerce_cart_order_is_cart', $is_cart, $order);
949

    
950
  return $is_cart;
951
}
952

    
953
/**
954
 * Implements hook_commerce_entity_access_condition_commerce_order_alter().
955
 *
956
 * This alter hook allows the Cart module to add conditions to the query used to
957
 * determine if a user has view access to a given order. The Cart module will
958
 * always grant users access to view their own carts (independent of any
959
 * permission settings) and also grants anonymous users access to view their
960
 * completed orders if they've been given the permission.
961
 */
962
function commerce_cart_commerce_entity_access_condition_commerce_order_alter(&$conditions, $context) {
963
  // Find the user's cart order ID and anonymous user's completed orders.
964
  $current_order_id = commerce_cart_order_id($context['account']->uid);
965
  $completed_order_ids = commerce_cart_order_session_order_ids(TRUE);
966

    
967
  // Always give the current user access to their own cart regardless of order
968
  // view permissions.
969
  if (!empty($current_order_id)) {
970
    $conditions->condition($context['base_table'] . '.order_id', $current_order_id);
971
  }
972

    
973
  // Bail now if the access query is for an authenticated user or if the
974
  // anonymous user doesn't have any completed orders.
975
  if ($context['account']->uid || empty($completed_order_ids)) {
976
    return;
977
  }
978

    
979
  // If the user has access to view his own orders of any bundle...
980
  if (user_access('view own ' . $context['entity_type'] . ' entities', $context['account'])) {
981
    // Add a condition granting the user view access to any completed orders
982
    // in his session.
983
    $conditions->condition($context['base_table'] . '.order_id', $completed_order_ids, 'IN');
984
  }
985

    
986
  // Add additional conditions on a per order bundle basis.
987
  $entity_info = entity_get_info($context['entity_type']);
988

    
989
  foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
990
    // Otherwise if the user has access to view his own entities of the current
991
    // bundle, add an AND condition group that grants access if the entity
992
    // specified by the view query matches the same bundle and belongs to the user.
993
    if (user_access('view own ' . $context['entity_type'] . ' entities of bundle ' . $bundle_name, $context['account'])) {
994
      $conditions->condition(db_and()
995
        ->condition($context['base_table'] . '.' . $entity_info['entity keys']['bundle'], $bundle_name)
996
        ->condition($context['base_table'] . '.order_id', $completed_order_ids, 'IN')
997
      );
998
    }
999
  }
1000
}
1001

    
1002
/**
1003
 * Converts an anonymous shopping cart order to an authenticated cart.
1004
 *
1005
 * @param $order
1006
 *   The anonymous order to convert to an authenticated cart.
1007
 * @param $account
1008
 *   The user account the order will belong to.
1009
 *
1010
 * @return
1011
 *   The updated order's wrapper or FALSE if the order was not converted,
1012
 *     meaning it was not an anonymous cart order to begin with.
1013
 */
1014
function commerce_cart_order_convert($order, $account) {
1015
  // Only convert orders that are currently anonmyous orders.
1016
  if ($order->uid == 0) {
1017
    // Update the uid and e-mail address to match the current account since
1018
    // there currently is no way to specify a custom e-mail address per order.
1019
    $order->uid = $account->uid;
1020
    $order->mail = $account->mail;
1021

    
1022
    // Update the uid of any referenced customer profiles.
1023
    $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
1024

    
1025
    foreach (field_info_instances('commerce_order', $order->type) as $field_name => $instance) {
1026
      $field_info = field_info_field($field_name);
1027

    
1028
      if ($field_info['type'] == 'commerce_customer_profile_reference') {
1029
        if ($order_wrapper->{$field_name} instanceof EntityListWrapper) {
1030
          foreach ($order_wrapper->{$field_name} as $delta => $profile_wrapper) {
1031
            if ($profile_wrapper->uid->value() == 0) {
1032
              $profile_wrapper->uid = $account->uid;
1033
              $profile_wrapper->save();
1034
            }
1035
          }
1036
        }
1037
        elseif (!is_null($order_wrapper->{$field_name}->value()) &&
1038
          $order_wrapper->{$field_name}->uid->value() == 0) {
1039
          $order_wrapper->{$field_name}->uid = $account->uid;
1040
          $order_wrapper->{$field_name}->save();
1041
        }
1042
      }
1043
    }
1044

    
1045
    // Allow other modules to operate on the converted order and then save.
1046
    module_invoke_all('commerce_cart_order_convert', $order_wrapper, $account);
1047
    $order_wrapper->save();
1048

    
1049
    return $order_wrapper;
1050
  }
1051

    
1052
  return FALSE;
1053
}
1054

    
1055
/**
1056
 * Refreshes the contents of a shopping cart by finding the most current prices
1057
 * for any product line items on the order.
1058
 *
1059
 * @param $order
1060
 *   The order object whose line items should be refreshed.
1061
 *
1062
 * @return
1063
 *   The updated order's wrapper.
1064
 */
1065
function commerce_cart_order_refresh($order) {
1066
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
1067

    
1068
  // Loop over every line item on the order...
1069
  $line_item_changed = FALSE;
1070

    
1071
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
1072
    // If the current line item actually no longer exists...
1073
    if (!$line_item_wrapper->value()) {
1074
      // Remove the reference from the order and continue to the next value.
1075
      $order_wrapper->commerce_line_items->offsetUnset($delta);
1076
      continue;
1077
    }
1078

    
1079
    // Knowing it exists, clone the line item now.
1080
    $cloned_line_item = clone($line_item_wrapper->value());
1081

    
1082
    // If the line item is a product line item...
1083
    if (in_array($cloned_line_item->type, commerce_product_line_item_types())) {
1084
      $product = $line_item_wrapper->commerce_product->value();
1085

    
1086
      // If this price has already been calculated, reset it to its original
1087
      // value so it can be recalculated afresh in the current context.
1088
      if (isset($product->commerce_price[LANGUAGE_NONE][0]['original'])) {
1089
        $original = $product->commerce_price[LANGUAGE_NONE][0]['original'];
1090
        foreach ($product->commerce_price as $langcode => $value) {
1091
          $product->commerce_price[$langcode] = array(0 => $original);
1092
        }
1093
      }
1094

    
1095
      // Repopulate the line item array with the default values for the product
1096
      // as though it had not been added to the cart yet, but preserve the
1097
      // current quantity and display URI information.
1098
      commerce_product_line_item_populate($cloned_line_item, $product);
1099

    
1100
      // Process the unit price through Rules so it reflects the user's actual
1101
      // current purchase price.
1102
      rules_invoke_event('commerce_product_calculate_sell_price', $cloned_line_item);
1103
    }
1104

    
1105
    // Allow other modules to alter line items on a shopping cart refresh.
1106
    module_invoke_all('commerce_cart_line_item_refresh', $cloned_line_item, $order_wrapper);
1107

    
1108
    // Delete this line item if it no longer has a valid price.
1109
    $current_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $cloned_line_item);
1110

    
1111
    if (is_null($current_line_item_wrapper->commerce_unit_price->value()) ||
1112
      is_null($current_line_item_wrapper->commerce_unit_price->amount->value()) ||
1113
      is_null($current_line_item_wrapper->commerce_unit_price->currency_code->value())) {
1114
      commerce_cart_order_product_line_item_delete($order, $cloned_line_item->line_item_id, TRUE);
1115
    }
1116
    else {
1117
      // Compare the refreshed unit price to the original unit price looking for
1118
      // differences in the amount, currency code, or price components.
1119
      $data = $line_item_wrapper->commerce_unit_price->data->value() + array('components' => array());
1120
      $current_data = (array) $current_line_item_wrapper->commerce_unit_price->data->value() + array('components' => array());
1121

    
1122
      if ($line_item_wrapper->commerce_unit_price->amount->value() != $current_line_item_wrapper->commerce_unit_price->amount->value() ||
1123
        $line_item_wrapper->commerce_unit_price->currency_code->value() != $current_line_item_wrapper->commerce_unit_price->currency_code->value() ||
1124
        $data['components'] != $current_data['components']) {
1125
        // Adjust the unit price accordingly if necessary.
1126
        $line_item_wrapper->commerce_unit_price->amount = $current_line_item_wrapper->commerce_unit_price->amount->value();
1127
        $line_item_wrapper->commerce_unit_price->currency_code = $current_line_item_wrapper->commerce_unit_price->currency_code->value();
1128

    
1129
        // Only migrate the price components in the data to preserve other data.
1130
        $data['components'] = $current_data['components'];
1131
        $line_item_wrapper->commerce_unit_price->data = $data;
1132

    
1133
        // Save the updated line item and clear the entity cache.
1134
        commerce_line_item_save($line_item_wrapper->value());
1135
        entity_get_controller('commerce_line_item')->resetCache(array($line_item_wrapper->line_item_id->value()));
1136

    
1137
        $line_item_changed = TRUE;
1138
      }
1139
    }
1140
  }
1141

    
1142
  // Store a copy of the original order to see if it changes later.
1143
  $original_order = clone($order_wrapper->value());
1144

    
1145
  // Allow other modules to alter the entire order on a shopping cart refresh.
1146
  module_invoke_all('commerce_cart_order_refresh', $order_wrapper);
1147

    
1148
  // Save the order once here if it has changed or if a line item was changed.
1149
  if ($order_wrapper->value() != $original_order || $line_item_changed) {
1150
    commerce_order_save($order_wrapper->value());
1151
  }
1152

    
1153
  return $order_wrapper;
1154
}
1155

    
1156
/**
1157
 * Entity metadata callback: returns the current user's shopping cart order.
1158
 *
1159
 * @see commerce_cart_entity_property_info_alter()
1160
 */
1161
function commerce_cart_get_properties($data = FALSE, array $options, $name) {
1162
  global $user;
1163

    
1164
  switch ($name) {
1165
    case 'current_cart_order':
1166
      if ($order = commerce_cart_order_load($user->uid)) {
1167
        return $order;
1168
      }
1169
      else {
1170
        return commerce_order_new($user->uid, 'cart');
1171
      }
1172
  }
1173
}
1174

    
1175
/**
1176
 * Returns an array of cart order IDs stored in the session.
1177
 *
1178
 * @param $completed
1179
 *   Boolean indicating whether or not the operation should retrieve the
1180
 *   completed orders array instead of the active cart orders array.
1181
 *
1182
 * @return
1183
 *   An array of applicable cart order IDs or an empty array if none exist.
1184
 */
1185
function commerce_cart_order_session_order_ids($completed = FALSE) {
1186
  $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
1187
  return empty($_SESSION[$key]) ? array() : $_SESSION[$key];
1188
}
1189

    
1190
/**
1191
 * Saves an order ID to the appropriate cart orders session variable.
1192
 *
1193
 * @param $order_id
1194
 *   The order ID to save to the array.
1195
 * @param $completed
1196
 *   Boolean indicating whether or not the operation should save to the
1197
 *     completed orders array instead of the active cart orders array.
1198
 */
1199
function commerce_cart_order_session_save($order_id, $completed = FALSE) {
1200
  $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
1201

    
1202
  if (empty($_SESSION[$key])) {
1203
    $_SESSION[$key] = array($order_id);
1204
  }
1205
  elseif (!in_array($order_id, $_SESSION[$key])) {
1206
    $_SESSION[$key][] = $order_id;
1207
  }
1208
}
1209

    
1210
/**
1211
 * Checks to see if any order ID or a specific order ID exists in the session.
1212
 *
1213
 * @param $order_id
1214
 *   Optionally specify an order ID to look for in the commerce_cart_orders
1215
 *     session variable; defaults to NULL.
1216
 * @param $completed
1217
 *   Boolean indicating whether or not the operation should look in the
1218
 *     completed orders array instead of the active cart orders array.
1219
 *
1220
 * @return
1221
 *   Boolean indicating whether or not any cart order ID exists in the session
1222
 *     or if the specified order ID exists in the session.
1223
 */
1224
function commerce_cart_order_session_exists($order_id = NULL, $completed = FALSE) {
1225
  $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
1226

    
1227
  // If an order was specified, look for it in the array.
1228
  if (!empty($order_id)) {
1229
    return !empty($_SESSION[$key]) && in_array($order_id, $_SESSION[$key]);
1230
  }
1231
  else {
1232
    // Otherwise look for any value.
1233
    return !empty($_SESSION[$key]);
1234
  }
1235
}
1236

    
1237
/**
1238
 * Deletes all order IDs or a specific order ID from the cart orders session
1239
 *   variable.
1240
 *
1241
 * @param $order_id
1242
 *   The order ID to remove from the array or NULL to delete the variable.
1243
 * @param $completed
1244
 *   Boolean indicating whether or not the operation should delete from the
1245
 *     completed orders array instead of the active cart orders array.
1246
 */
1247
function commerce_cart_order_session_delete($order_id = NULL, $completed = FALSE) {
1248
  $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
1249

    
1250
  if (!empty($_SESSION[$key])) {
1251
    if (!empty($order_id)) {
1252
      $_SESSION[$key] = array_diff($_SESSION[$key], array($order_id));
1253
    }
1254
    else {
1255
      unset($_SESSION[$key]);
1256
    }
1257
  }
1258
}
1259

    
1260
/**
1261
 * Adds the specified product to a customer's shopping cart.
1262
 *
1263
 * @param $uid
1264
 *   The uid of the user whose cart you are adding the product to.
1265
 * @param $line_item
1266
 *   An unsaved product line item to be added to the cart with the following data
1267
 *   on the line item being used to determine how to add the product to the cart:
1268
 *   - $line_item->commerce_product: reference to the product to add to the cart.
1269
 *   - $line_item->quantity: quantity of this product to add to the cart.
1270
 *   - $line_item->data: data array that is saved with the line item if the line
1271
 *     item is added to the cart as a new item; merged into an existing line
1272
 *     item if combination is possible.
1273
 *   - $line_item->order_id: this property does not need to be set when calling
1274
 *     this function, as it will be set to the specified user's current cart
1275
 *     order ID.
1276
 *   Additional field data on the line item may be considered when determining
1277
 *   whether or not line items can be combined in the cart. This includes the
1278
 *   line item type, referenced product, and any line item fields that have been
1279
 *   exposed on the Add to Cart form.
1280
 * @param $combine
1281
 *   Boolean indicating whether or not to combine like products on the same line
1282
 *   item, incrementing an existing line item's quantity instead of adding a
1283
 *   new line item to the cart order. When the incoming line item is combined
1284
 *   into an existing line item, field data on the existing line item will be
1285
 *   left unchanged. Only the quantity will be incremented and the data array
1286
 *   will be updated by merging the data from the existing line item onto the
1287
 *   data from the incoming line item, giving precedence to the most recent data.
1288
 *
1289
 * @return
1290
 *   The new or updated line item object or FALSE on failure.
1291
 */
1292
function commerce_cart_product_add($uid, $line_item, $combine = TRUE) {
1293
  // Do not add the line item if it doesn't have a unit price.
1294
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
1295

    
1296
  if (is_null($line_item_wrapper->commerce_unit_price->value())) {
1297
    return FALSE;
1298
  }
1299

    
1300
  // First attempt to load the customer's shopping cart order.
1301
  $order = commerce_cart_order_load($uid);
1302

    
1303
  // If no order existed, create one now.
1304
  if (empty($order)) {
1305
    $order = commerce_cart_order_new($uid);
1306
    $order->data['last_cart_refresh'] = REQUEST_TIME;
1307
  }
1308

    
1309
  // Set the incoming line item's order_id.
1310
  $line_item->order_id = $order->order_id;
1311

    
1312
  // Wrap the order for easy access to field data.
1313
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
1314

    
1315
  // Extract the product and quantity we're adding from the incoming line item.
1316
  $product = $line_item_wrapper->commerce_product->value();
1317
  $quantity = $line_item->quantity;
1318

    
1319
  // Invoke the product prepare event with the shopping cart order.
1320
  rules_invoke_all('commerce_cart_product_prepare', $order, $product, $line_item->quantity);
1321

    
1322
  // Determine if the product already exists on the order and increment its
1323
  // quantity instead of adding a new line if it does.
1324
  $matching_line_item = NULL;
1325

    
1326
  // If we are supposed to look for a line item to combine into...
1327
  if ($combine) {
1328
    // Generate an array of properties and fields to compare.
1329
    $comparison_properties = array('type', 'commerce_product');
1330

    
1331
    // Add any field that was exposed on the Add to Cart form to the array.
1332
    // TODO: Bypass combination when an exposed field is no longer available but
1333
    // the same base product is added to the cart.
1334
    foreach (field_info_instances('commerce_line_item', $line_item->type) as $info) {
1335
      if (!empty($info['commerce_cart_settings']['field_access'])) {
1336
        $comparison_properties[] = $info['field_name'];
1337
      }
1338
    }
1339

    
1340
    // Allow other modules to specify what properties should be compared when
1341
    // determining whether or not to combine line items.
1342
    drupal_alter('commerce_cart_product_comparison_properties', $comparison_properties, clone($line_item));
1343

    
1344
    // Loop over each line item on the order.
1345
    foreach ($order_wrapper->commerce_line_items as $delta => $matching_line_item_wrapper) {
1346
      // Examine each of the comparison properties on the line item.
1347
      foreach ($comparison_properties as $property) {
1348
        // If the property is not present on either line item, bypass it.
1349
        if (!isset($matching_line_item_wrapper->value()->{$property}) && !isset($line_item_wrapper->value()->{$property})) {
1350
          continue;
1351
        }
1352

    
1353
        // If any property does not match the same property on the incoming line
1354
        // item or exists on one line item but not the other...
1355
        if ((!isset($matching_line_item_wrapper->value()->{$property}) && isset($line_item_wrapper->value()->{$property})) ||
1356
          (isset($matching_line_item_wrapper->value()->{$property}) && !isset($line_item_wrapper->value()->{$property})) ||
1357
          $matching_line_item_wrapper->{$property}->raw() != $line_item_wrapper->{$property}->raw()) {
1358
          // Continue the loop with the next line item.
1359
          continue 2;
1360
        }
1361
      }
1362

    
1363
      // If every comparison line item matched, combine into this line item.
1364
      $matching_line_item = $matching_line_item_wrapper->value();
1365
      break;
1366
    }
1367
  }
1368

    
1369
  // If no matching line item was found...
1370
  if (empty($matching_line_item)) {
1371
    // Save the incoming line item now so we get its ID.
1372
    commerce_line_item_save($line_item);
1373

    
1374
    // Add it to the order's line item reference value.
1375
    $order_wrapper->commerce_line_items[] = $line_item;
1376
  }
1377
  else {
1378
    // Increment the quantity of the matching line item, update the data array,
1379
    // and save it.
1380
    $matching_line_item->quantity += $quantity;
1381
    $matching_line_item->data = array_merge($line_item->data, $matching_line_item->data);
1382

    
1383
    commerce_line_item_save($matching_line_item);
1384

    
1385
    // Clear the line item cache so the updated quantity will be available to
1386
    // the ensuing load instead of the original quantity as loaded above.
1387
    entity_get_controller('commerce_line_item')->resetCache(array($matching_line_item->line_item_id));
1388

    
1389
    // Update the line item variable for use in the invocation and return value.
1390
    $line_item = $matching_line_item;
1391
  }
1392

    
1393
  // Save the updated order.
1394
  commerce_order_save($order);
1395

    
1396
  // Invoke the product add event with the newly saved or updated line item.
1397
  rules_invoke_all('commerce_cart_product_add', $order, $product, $quantity, $line_item);
1398

    
1399
  // Return the line item.
1400
  return $line_item;
1401
}
1402

    
1403
/**
1404
 * Adds the specified product to a customer's shopping cart by product ID.
1405
 *
1406
 * This function is merely a helper function that builds a line item for the
1407
 * specified product ID and adds it to a shopping cart. It does not offer the
1408
 * full support of product line item fields that commerce_cart_product_add()
1409
 * does, so you may still need to use the full function, especially if you need
1410
 * to specify display_path field values or interact with custom line item fields.
1411
 *
1412
 * @param $product_id
1413
 *   ID of the product to add to the cart.
1414
 * @param $quantity
1415
 *   Quantity of the specified product to add to the cart; defaults to 1.
1416
 * @param $combine
1417
 *   Boolean indicating whether or not to combine like products on the same line
1418
 *   item, incrementing an existing line item's quantity instead of adding a
1419
 *   new line item to the cart order.
1420
 * @param $uid
1421
 *   User ID of the shopping cart owner the whose cart the product should be
1422
 *   added to; defaults to the current user.
1423
 *
1424
 * @return
1425
 *   A new or updated line item object representing the product in the cart or
1426
 *   FALSE on failure.
1427
 *
1428
 * @see commerce_cart_product_add()
1429
 */
1430
function commerce_cart_product_add_by_id($product_id, $quantity = 1, $combine = TRUE, $uid = NULL) {
1431
  global $user;
1432

    
1433
  // If the specified product exists...
1434
  if ($product = commerce_product_load($product_id)) {
1435
    // Create a new product line item for it.
1436
    $line_item = commerce_product_line_item_new($product, $quantity);
1437

    
1438
    // Default to the current user if a uid was not passed in.
1439
    if ($uid === NULL) {
1440
      $uid = $user->uid;
1441
    }
1442

    
1443
    return commerce_cart_product_add($uid, $line_item, $combine);
1444
  }
1445

    
1446
  return FALSE;
1447
}
1448

    
1449
/**
1450
 * Deletes a product line item from a shopping cart order.
1451
 *
1452
 * @param $order
1453
 *   The shopping cart order to delete from.
1454
 * @param $line_item_id
1455
 *   The ID of the product line item to delete from the order.
1456
 * @param $skip_save
1457
 *   TRUE to skip saving the order after deleting the line item; used when the
1458
 *     order would otherwise be saved or to delete multiple product line items
1459
 *     from the order and then save.
1460
 *
1461
 * @return
1462
 *   The order with the matching product line item deleted from the line item
1463
 *     reference field.
1464
 */
1465
function commerce_cart_order_product_line_item_delete($order, $line_item_id, $skip_save = FALSE) {
1466
  $line_item = commerce_line_item_load($line_item_id);
1467

    
1468
  // Check to ensure the line item exists and is a product line item.
1469
  if (!$line_item || !in_array($line_item->type, commerce_product_line_item_types())) {
1470
    return $order;
1471
  }
1472

    
1473
  // Remove the line item from the line item reference field.
1474
  commerce_entity_reference_delete($order, 'commerce_line_items', 'line_item_id', $line_item_id);
1475

    
1476
  // Wrap the line item to be deleted and extract the product from it.
1477
  $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
1478
  $product = $wrapper->commerce_product->value();
1479

    
1480
  // Invoke the product removal event with the line item about to be deleted.
1481
  rules_invoke_all('commerce_cart_product_remove', $order, $product, $line_item->quantity, $line_item);
1482

    
1483
  // Delete the actual line item.
1484
  commerce_line_item_delete($line_item->line_item_id);
1485

    
1486
  if (!$skip_save) {
1487
    commerce_order_save($order);
1488
  }
1489

    
1490
  return $order;
1491
}
1492

    
1493
/**
1494
 * Deletes every product line item from a shopping cart order.
1495
 *
1496
 * @param $order
1497
 *   The shopping cart order to empty.
1498
 *
1499
 * @return
1500
 *   The order with the product line items all removed.
1501
 */
1502
function commerce_cart_order_empty($order) {
1503
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
1504

    
1505
  // Build an array of product line item IDs.
1506
  $line_item_ids = array();
1507

    
1508
  foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
1509
    $line_item_ids[] = $line_item_wrapper->line_item_id->value();
1510
  }
1511

    
1512
  // Delete each line item one by one from the order. This is done this way
1513
  // instead of unsetting each as we find it to ensure that changing delta
1514
  // values don't prevent an item from being removed from the order.
1515
  foreach ($line_item_ids as $line_item_id) {
1516
    $order = commerce_cart_order_product_line_item_delete($order, $line_item_id, TRUE);
1517
  }
1518

    
1519
  // Allow other modules to update the order on empty prior to save.
1520
  module_invoke_all('commerce_cart_order_empty', $order);
1521

    
1522
  // Save and return the order.
1523
  commerce_order_save($order);
1524

    
1525
  return $order;
1526
}
1527

    
1528
/**
1529
 * Determines whether or not the given field is eligible to function as a
1530
 * product attribute field on the Add to Cart form.
1531
 *
1532
 * @param $field
1533
 *   The info array of the field whose eligibility you want to determine.
1534
 *
1535
 * @return
1536
 *   TRUE or FALSE indicating the field's eligibility.
1537
 */
1538
function commerce_cart_field_attribute_eligible($field) {
1539
  // Returns TRUE if the field is single value (i.e. has a cardinality of 1) and
1540
  // is defined by a module implementing hook_options_list() to provide an array
1541
  // of allowed values structured as human-readable option names keyed by value.
1542
  return $field['cardinality'] == 1 && function_exists($field['module'] . '_options_list');
1543
}
1544

    
1545
/**
1546
 * Returns an array of attribute settings for a field instance.
1547
 *
1548
 * Fields attached to product types may be used as product attribute fields with
1549
 * selection widgets on Add to Cart forms. This function returns the default
1550
 * values for a given field instance.
1551
 *
1552
 * @param $instance
1553
 *   The info array of the field instance whose attribute settings should be
1554
 *   retrieved.
1555
 *
1556
 * @return
1557
 *   An array of attribute settings including:
1558
 *   - attribute_field: boolean indicating whether or not the instance should
1559
 *     be used as a product attribute field on the Add to Cart form; defaults
1560
 *     to FALSE
1561
 *   - attribute_widget: string indicating the type of form element to use on
1562
 *     the Add to Cart form for customers to select the attribute option;
1563
 *     defaults to 'select', may also be 'radios'
1564
 *   - attribute_widget_title: string used as the title of the attribute form
1565
 *     element on the Add to Cart form.
1566
 */
1567
function commerce_cart_field_instance_attribute_settings($instance) {
1568
  if (empty($instance['commerce_cart_settings']) || !is_array($instance['commerce_cart_settings'])) {
1569
    $commerce_cart_settings = array();
1570
  }
1571
  else {
1572
    $commerce_cart_settings = $instance['commerce_cart_settings'];
1573
  }
1574

    
1575
  // Supply default values for the cart settings pertaining here to
1576
  // product attribute fields.
1577
  $commerce_cart_settings += array(
1578
    'attribute_field' => FALSE,
1579
    'attribute_widget' => 'select',
1580
    'attribute_widget_title' => '',
1581
  );
1582

    
1583
  return $commerce_cart_settings;
1584
}
1585

    
1586
/**
1587
 * Determines whether or not a field instance is fucntioning as a product
1588
 * attribute field.
1589
 *
1590
 * @param $instance
1591
 *   The instance info array for the field instance.
1592
 *
1593
 * @return
1594
 *   Boolean indicating whether or not the field instance is an attribute field.
1595
 */
1596
function commerce_cart_field_instance_is_attribute($instance) {
1597
  $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
1598
  return !empty($commerce_cart_settings['attribute_field']);
1599
}
1600

    
1601
/**
1602
 * Returns an array of cart form field access settings for a field instance.
1603
 *
1604
 * Fields attached to line item types can be included on the Add to Cart form so
1605
 * customers can supply additional information for the line item when it is
1606
 * added to the cart. Certain fields will not be exposed based on line item
1607
 * field access integration, such as the total price field which is always
1608
 * computationally generated on line item save.
1609
 *
1610
 * @param $instance
1611
 *   The info array of the field instance whose field access settings should be
1612
 *   retrieved.
1613
 *
1614
 * @return
1615
 *   An array of field access settings including:
1616
 *   - field_access: boolean indicating whether or not this field instance
1617
 *     should appear on the Add to Cart form.
1618
 */
1619
function commerce_cart_field_instance_access_settings($instance) {
1620
  if (empty($instance['commerce_cart_settings']) || !is_array($instance['commerce_cart_settings'])) {
1621
    $commerce_cart_settings = array();
1622
  }
1623
  else {
1624
    $commerce_cart_settings = $instance['commerce_cart_settings'];
1625
  }
1626

    
1627
  // Supply default values for the cart settings pertaining here to field access
1628
  // on the Add to Cart form.
1629
  $commerce_cart_settings += array(
1630
    'field_access' => FALSE,
1631
  );
1632

    
1633
  return $commerce_cart_settings;
1634
}
1635

    
1636
/**
1637
 * Returns the title of an attribute widget for the Add to Cart form.
1638
 *
1639
 * @param $instance
1640
 *   The attribute field instance info array.
1641
 *
1642
 * @return
1643
 *   A sanitized string to use as the attribute widget title.
1644
 */
1645
function commerce_cart_attribute_widget_title($instance) {
1646
  // Translate the entire field instance and find the default title.
1647
  $translated_instance = commerce_i18n_object('field_instance', $instance);
1648
  $title = $translated_instance['label'];
1649

    
1650
  // Use the customized attribute widget title if it exists.
1651
  $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
1652

    
1653
  if (!empty($commerce_cart_settings['attribute_widget_title'])) {
1654
    $title = $commerce_cart_settings['attribute_widget_title'];
1655

    
1656
    // Use the translated customized title if it exists.
1657
    if (!empty($translated_instance['attribute_widget_title'])) {
1658
      $title = $translated_instance['attribute_widget_title'];
1659
    }
1660
  }
1661

    
1662
  return check_plain($title);
1663
}
1664

    
1665
/**
1666
 * Builds an appropriate cart form ID based on the products on the form.
1667
 *
1668
 * @see commerce_cart_forms().
1669
 */
1670
function commerce_cart_add_to_cart_form_id($product_ids) {
1671
  // Make sure the length of the form id is limited.
1672
  $data = implode('_', $product_ids);
1673

    
1674
  if (strlen($data) > 50) {
1675
    $data = drupal_hash_base64($data);
1676
  }
1677

    
1678
  return 'commerce_cart_add_to_cart_form_' . $data;
1679
}
1680

    
1681
/**
1682
 * Implements hook_forms().
1683
 *
1684
 * To provide distinct form IDs for add to cart forms, the product IDs
1685
 * referenced by the form are appended to the base ID,
1686
 * commerce_cart_add_to_cart_form. When such a form is built or submitted, this
1687
 * function will return the proper callback function to use for the given form.
1688
 */
1689
function commerce_cart_forms($form_id, $args) {
1690
  $forms = array();
1691

    
1692
  // Construct a valid cart form ID from the arguments.
1693
  if (strpos($form_id, 'commerce_cart_add_to_cart_form_') === 0) {
1694
    $forms[$form_id] = array(
1695
      'callback' => 'commerce_cart_add_to_cart_form',
1696
    );
1697
  }
1698

    
1699
  return $forms;
1700
}
1701

    
1702
/**
1703
 * Builds an Add to Cart form for a set of products.
1704
 *
1705
 * @param $line_item
1706
 *   A fully formed product line item whose data will be used in the following
1707
 *   ways by the form:
1708
 *   - $line_item->data['context']['product_ids']: an array of product IDs to
1709
 *     include on the form or the string 'entity' if the context array includes
1710
 *     an entity array with information for accessing the product IDs from an
1711
 *     entity's product reference field.
1712
 *   - $line_item->data['context']['entity']: if the product_ids value is the
1713
 *     string 'entity', an associative array with the keys 'entity_type',
1714
 *     'entity_id', and 'product_reference_field_name' that points to the
1715
 *     location of the product IDs used to build the form.
1716
 *   - $line_item->data['context']['add_to_cart_combine']: a boolean indicating
1717
 *     whether or not to attempt to combine the product added to the cart with
1718
 *     existing line items of matching fields.
1719
 *   - $line_item->data['context']['show_single_product_attributes']: a boolean
1720
 *     indicating whether or not product attribute fields with single options
1721
 *     should be shown on the Add to Cart form.
1722
 *   - $line_item->quantity: the default value for the quantity widget if
1723
 *     included (determined by the $show_quantity parameter).
1724
 *   - $line_item->commerce_product: the value of this field will be used as the
1725
 *     default product ID when the form is built for multiple products.
1726
 *   The line item's data array will be used on submit to set the data array of
1727
 *   the product line item created by the form.
1728
 * @param $show_quantity
1729
 *   Boolean indicating whether or not to show the quantity widget; defaults to
1730
 *   FALSE resulting in a hidden field holding the quantity.
1731
 * @param $context
1732
 *   Information on the context of the form's placement, allowing it to update
1733
 *   product fields on the page based on the currently selected default product.
1734
 *   Should be an associative array containing the following keys:
1735
 *   - class_prefix: a prefix used to target HTML containers for replacement
1736
 *     with rendered fields as the default product is updated. For example,
1737
 *     nodes display product fields in their context wrapped in spans with the
1738
 *     class node-#-product-field_name.  The class_prefix for the add to cart
1739
 *     form displayed on a node would be node-# with this form's AJAX refresh
1740
 *     adding the suffix -product-field_name.
1741
 *   - view_mode: a product view mode that tells the AJAX refresh how to render
1742
 *     the replacement fields.
1743
 *   If no context is specified, AJAX replacement of rendered fields will not
1744
 *   happen. This parameter only affects forms containing multiple products.
1745
 *
1746
 * @return
1747
 *   The form array.
1748
 */
1749
function commerce_cart_add_to_cart_form($form, &$form_state, $line_item, $show_quantity = FALSE, $context = array()) {
1750
  global $user;
1751

    
1752
  // Store the context in the form state for use during AJAX refreshes.
1753
  $form_state['context'] = $context;
1754

    
1755
  // Store the line item passed to the form builder for reference on submit.
1756
  $form_state['line_item'] = $line_item;
1757
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
1758
  $default_quantity = $line_item->quantity;
1759

    
1760
  // Retrieve the array of product IDs from the line item's context data array.
1761
  $product_ids = commerce_cart_add_to_cart_form_product_ids($line_item);
1762

    
1763
  // If we don't have a list of products to load, just bail out early.
1764
  // There is nothing we can or have to do in that case.
1765
  if (empty($product_ids)) {
1766
    return array();
1767
  }
1768

    
1769
  // Add a generic class ID.
1770
  $form['#attributes']['class'][] = drupal_html_class('commerce-add-to-cart');
1771

    
1772
  // Store the form ID as a class of the form to avoid the incrementing form ID
1773
  // from causing the AJAX refresh not to work.
1774
  $form['#attributes']['class'][] = drupal_html_class(commerce_cart_add_to_cart_form_id($product_ids));
1775

    
1776
  // Store the customer uid in the form so other modules can override with a
1777
  // selection widget if necessary.
1778
  $form['uid'] = array(
1779
    '#type' => 'value',
1780
    '#value' => $user->uid,
1781
  );
1782

    
1783
  // Load all the active products intended for sale on this form.
1784
  $products = commerce_product_load_multiple($product_ids, array('status' => 1));
1785

    
1786
  // If no products were returned...
1787
  if (count($products) == 0) {
1788
    $form['submit'] = array(
1789
      '#type' => 'submit',
1790
      '#value' => t('Product not available'),
1791
      '#weight' => 15,
1792
      // Do not set #disabled in order not to prevent submission.
1793
      '#attributes' => array('disabled' => 'disabled'),
1794
      '#validate' => array('commerce_cart_add_to_cart_form_disabled_validate'),
1795
    );
1796
  }
1797
  else {
1798
    // If the form is for a single product and displaying attributes on a single
1799
    // product Add to Cart form is disabled in the form context, store the
1800
    // product_id in a hidden form field for use by the submit handler.
1801
    if (count($products) == 1 && empty($line_item->data['context']['show_single_product_attributes'])) {
1802
      $form_state['default_product'] = reset($products);
1803

    
1804
      $form['product_id'] = array(
1805
        '#type' => 'hidden',
1806
        '#value' => key($products),
1807
      );
1808
    }
1809
    else {
1810
      // However, if more than one products are represented on it, attempt to
1811
      // use smart select boxes for the product selection. If the products are
1812
      // all of the same type and there are qualifying fields on that product
1813
      // type, display their options for customer selection.
1814
      $qualifying_fields = array();
1815
      $same_type = TRUE;
1816
      $type = '';
1817

    
1818
      // Find the default product so we know how to set default options on the
1819
      // various Add to Cart form widgets and an array of any matching product
1820
      // based on attribute selections so we can add a selection widget.
1821
      $matching_products = array();
1822
      $default_product = NULL;
1823
      $attribute_names = array();
1824
      $unchanged_attributes = array();
1825

    
1826
      foreach ($products as $product_id => $product) {
1827
        $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
1828

    
1829
        // Store the first product type.
1830
        if (empty($type)) {
1831
          $type = $product->type;
1832
        }
1833

    
1834
        // If the current product type is different from the first, we are not
1835
        // dealing with a set of same typed products.
1836
        if ($product->type != $type) {
1837
          $same_type = FALSE;
1838
        }
1839

    
1840
        // If the form state contains a set of attribute data, use it to try
1841
        // and determine the default product.
1842
        $changed_attribute = NULL;
1843

    
1844
        if (!empty($form_state['values']['attributes'])) {
1845
          $match = TRUE;
1846

    
1847
          // Set an array of checked attributes for later comparison against the
1848
          // default matching product.
1849
          if (empty($attribute_names)) {
1850
            $attribute_names = (array) array_diff_key($form_state['values']['attributes'], array('product_select' => ''));
1851
            $unchanged_attributes = $form_state['values']['unchanged_attributes'];
1852
          }
1853

    
1854
          foreach ($attribute_names as $key => $value) {
1855
            // If this is the attribute widget that was changed...
1856
            if ($value != $unchanged_attributes[$key]) {
1857
              // Store the field name.
1858
              $changed_attribute = $key;
1859

    
1860
              // Clear the input for the "Select a product" widget now if it
1861
              // exists on the form since we know an attribute was changed.
1862
              unset($form_state['input']['attributes']['product_select']);
1863
            }
1864

    
1865
            // If a field name has been stored and we've moved past it to
1866
            // compare the next attribute field...
1867
            if (!empty($changed_attribute) && $changed_attribute != $key) {
1868
              // Wipe subsequent values from the form state so the attribute
1869
              // widgets can use the default values from the new default product.
1870
              unset($form_state['input']['attributes'][$key]);
1871

    
1872
              // Don't accept this as a matching product.
1873
              continue;
1874
            }
1875

    
1876
            if ($product_wrapper->{$key}->raw() != $value) {
1877
              $match = FALSE;
1878
            }
1879
          }
1880

    
1881
          // If the changed field name has already been stored, only accept the
1882
          // first matching product by ignoring the rest that would match. An
1883
          // exception is granted for additional matching products that share
1884
          // the exact same attribute values as the first.
1885
          if ($match && !empty($changed_attribute) && !empty($matching_products)) {
1886
            reset($matching_products);
1887
            $matching_product = $matching_products[key($matching_products)];
1888
            $matching_product_wrapper = entity_metadata_wrapper('commerce_product', $matching_product);
1889

    
1890
            foreach ($attribute_names as $key => $value) {
1891
              if ($product_wrapper->{$key}->raw() != $matching_product_wrapper->{$key}->raw()) {
1892
                $match = FALSE;
1893
              }
1894
            }
1895
          }
1896

    
1897
          if ($match) {
1898
            $matching_products[$product_id] = $product;
1899
          }
1900
        }
1901
      }
1902

    
1903
      // Set the default product now if it isn't already set.
1904
      if (empty($matching_products)) {
1905
        // If a product ID value was passed in, use that product if it exists.
1906
        if (!empty($form_state['values']['product_id']) &&
1907
          !empty($products[$form_state['values']['product_id']])) {
1908
          $default_product = $products[$form_state['values']['product_id']];
1909
        }
1910
        elseif (empty($form_state['values']) &&
1911
          !empty($line_item_wrapper->commerce_product) &&
1912
          !empty($products[$line_item_wrapper->commerce_product->raw()])) {
1913
          // If this is the first time the form is built, attempt to use the
1914
          // product specified by the line item.
1915
          $default_product = $products[$line_item_wrapper->commerce_product->raw()];
1916
        }
1917
        else {
1918
          reset($products);
1919
          $default_product = $products[key($products)];
1920
        }
1921
      }
1922
      else {
1923
        // If the product selector has a value, use that.
1924
        if (!empty($form_state['values']['attributes']['product_select']) &&
1925
            !empty($products[$form_state['values']['attributes']['product_select']]) &&
1926
            in_array($products[$form_state['values']['attributes']['product_select']], $matching_products)) {
1927
          $default_product = $products[$form_state['values']['attributes']['product_select']];
1928
        }
1929
        else {
1930
          reset($matching_products);
1931
          $default_product = $matching_products[key($matching_products)];
1932
        }
1933
      }
1934

    
1935
      // Wrap the default product for later use.
1936
      $default_product_wrapper = entity_metadata_wrapper('commerce_product', $default_product);
1937

    
1938
      $form_state['default_product'] = $default_product;
1939

    
1940
      // If all the products are of the same type...
1941
      if ($same_type) {
1942
        // Loop through all the field instances on that product type.
1943
        foreach (field_info_instances('commerce_product', $type) as $field_name => $instance) {
1944
          // A field qualifies if it is single value, required and uses a widget
1945
          // with a definite set of options. For the sake of simplicity, this is
1946
          // currently restricted to fields defined by the options module.
1947
          $field = field_info_field($field_name);
1948

    
1949
          // If the instance is of a field type that is eligible to function as
1950
          // a product attribute field and if its attribute field settings
1951
          // specify that this functionality is enabled...
1952
          if (commerce_cart_field_attribute_eligible($field) && commerce_cart_field_instance_is_attribute($instance)) {
1953
            // Get the options properties from the options module for the
1954
            // attribute widget type selected for the field, defaulting to the
1955
            // select list options properties.
1956
            $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
1957

    
1958
            switch ($commerce_cart_settings['attribute_widget']) {
1959
              case 'checkbox':
1960
                $widget_type = 'onoff';
1961
                break;
1962
              case 'radios':
1963
                $widget_type = 'buttons';
1964
                break;
1965
              default:
1966
                $widget_type = 'select';
1967
            }
1968

    
1969
            $properties = _options_properties($widget_type, FALSE, TRUE, TRUE);
1970

    
1971
            // Try to fetch localized names.
1972
            $allowed_values = NULL;
1973

    
1974
            // Prepare translated options if using the i18n_field module.
1975
            if (module_exists('i18n_field')) {
1976
              if (($translate = i18n_field_type_info($field['type'], 'translate_options'))) {
1977
                $allowed_values = $translate($field);
1978
                _options_prepare_options($allowed_values, $properties);
1979
              }
1980
            }
1981

    
1982
            // Otherwise just use the base language values.
1983
            if (empty($allowed_values)) {
1984
              $allowed_values = _options_get_options($field, $instance, $properties, 'commerce_product', $default_product);
1985
            }
1986

    
1987
            // Only consider this field a qualifying attribute field if we could
1988
            // derive a set of options for it.
1989
            if (!empty($allowed_values)) {
1990
              $qualifying_fields[$field_name] = array(
1991
                'field' => $field,
1992
                'instance' => $instance,
1993
                'commerce_cart_settings' => $commerce_cart_settings,
1994
                'options' => $allowed_values,
1995
                'weight' => $instance['widget']['weight'],
1996
                'required' => $instance['required'],
1997
              );
1998
            }
1999
          }
2000
        }
2001
      }
2002

    
2003
      // Otherwise for products of varying types, display a simple select list
2004
      // by product title.
2005
      if (!empty($qualifying_fields)) {
2006
        $used_options = array();
2007
        $field_has_options = array();
2008

    
2009
        // Sort the fields by weight.
2010
        uasort($qualifying_fields, 'drupal_sort_weight');
2011

    
2012
        foreach ($qualifying_fields as $field_name => $data) {
2013
          // Build an options array of widget options used by referenced products.
2014
          foreach ($products as $product_id => $product) {
2015
            $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
2016

    
2017
            // Only add options to the present array that appear on products that
2018
            // match the default value of the previously added attribute widgets.
2019
            foreach ($used_options as $used_field_name => $unused) {
2020
              // Don't apply this check for the current field being evaluated.
2021
              if ($used_field_name == $field_name) {
2022
                continue;
2023
              }
2024

    
2025
              if (isset($form['attributes'][$used_field_name]['#default_value'])) {
2026
                if ($product_wrapper->{$used_field_name}->raw() != $form['attributes'][$used_field_name]['#default_value']) {
2027
                  continue 2;
2028
                }
2029
              }
2030
            }
2031

    
2032
            // With our hard dependency on widgets provided by the Options
2033
            // module, we can make assumptions about where the data is stored.
2034
            if ($product_wrapper->{$field_name}->raw() != NULL) {
2035
              $field_has_options[$field_name] = TRUE;
2036
            }
2037
            $used_options[$field_name][] = $product_wrapper->{$field_name}->raw();
2038
          }
2039

    
2040
          // If for some reason no options for this field are used, remove it
2041
          // from the qualifying fields array.
2042
          if (empty($field_has_options[$field_name]) || empty($used_options[$field_name])) {
2043
            unset($qualifying_fields[$field_name]);
2044
          }
2045
          else {
2046
            $form['attributes'][$field_name] = array(
2047
              '#type' => $data['commerce_cart_settings']['attribute_widget'],
2048
              '#title' => commerce_cart_attribute_widget_title($data['instance']),
2049
              '#options' => array_intersect_key($data['options'], drupal_map_assoc($used_options[$field_name])),
2050
              '#default_value' => $default_product_wrapper->{$field_name}->raw(),
2051
              '#weight' => $data['instance']['widget']['weight'],
2052
              '#ajax' => array(
2053
                'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
2054
              ),
2055
            );
2056

    
2057
            // Add the empty value if the field is not required and products on
2058
            // the form include the empty value.
2059
            if (!$data['required'] && in_array('', $used_options[$field_name])) {
2060
              $form['attributes'][$field_name]['#empty_value'] = '';
2061
            }
2062

    
2063
            $form['unchanged_attributes'][$field_name] = array(
2064
              '#type' => 'value',
2065
              '#value' => $default_product_wrapper->{$field_name}->raw(),
2066
            );
2067
          }
2068
        }
2069

    
2070
        if (!empty($form['attributes'])) {
2071
          $form['attributes'] += array(
2072
            '#tree' => 'TRUE',
2073
            '#prefix' => '<div class="attribute-widgets">',
2074
            '#suffix' => '</div>',
2075
            '#weight' => 0,
2076
          );
2077
          $form['unchanged_attributes'] += array(
2078
            '#tree' => 'TRUE',
2079
          );
2080

    
2081
          // If the matching products array is empty, it means this is the first
2082
          // time the form is being built. We should populate it now with
2083
          // products that match the default attribute options.
2084
          if (empty($matching_products)) {
2085
            foreach ($products as $product_id => $product) {
2086
              $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
2087
              $match = TRUE;
2088

    
2089
              foreach (element_children($form['attributes']) as $field_name) {
2090
                if ($product_wrapper->{$field_name}->raw() != $form['attributes'][$field_name]['#default_value']) {
2091
                  $match = FALSE;
2092
                }
2093
              }
2094

    
2095
              if ($match) {
2096
                $matching_products[$product_id] = $product;
2097
              }
2098
            }
2099
          }
2100

    
2101
          // If there were more than one matching products for the current
2102
          // attribute selection, add a product selection widget.
2103
          if (count($matching_products) > 1) {
2104
            $options = array();
2105

    
2106
            foreach ($matching_products as $product_id => $product) {
2107
              $options[$product_id] = $product->title;
2108
            }
2109

    
2110
            // Note that this element by default is a select list, so its
2111
            // #options are not sanitized here. Sanitization will occur in a
2112
            // check_plain() in the function form_select_options(). If you alter
2113
            // this element to another #type, such as 'radios', you are also
2114
            // responsible for looping over its #options array and sanitizing
2115
            // the values.
2116
            $form['attributes']['product_select'] = array(
2117
              '#type' => 'select',
2118
              '#title' => t('Select a product'),
2119
              '#options' => $options,
2120
              '#default_value' => $default_product->product_id,
2121
              '#weight' => 40,
2122
              '#ajax' => array(
2123
                'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
2124
              ),
2125
            );
2126
          }
2127

    
2128
          $form['product_id'] = array(
2129
            '#type' => 'hidden',
2130
            '#value' => $default_product->product_id,
2131
          );
2132
        }
2133
      }
2134

    
2135
      // If the products referenced were of different types or did not posess
2136
      // any qualifying attribute fields...
2137
      if (!$same_type || empty($qualifying_fields)) {
2138
        // For a single product form, just add the hidden product_id field.
2139
        if (count($products) == 1) {
2140
          $form['product_id'] = array(
2141
            '#type' => 'hidden',
2142
            '#value' => $default_product->product_id,
2143
          );
2144
        }
2145
        else {
2146
          // Otherwise add a product selection widget.
2147
          $options = array();
2148

    
2149
          foreach ($products as $product_id => $product) {
2150
            $options[$product_id] = $product->title;
2151
          }
2152

    
2153
          // Note that this element by default is a select list, so its #options
2154
          // are not sanitized here. Sanitization will occur in a check_plain() in
2155
          // the function form_select_options(). If you alter this element to
2156
          // another #type, such as 'radios', you are also responsible for looping
2157
          // over its #options array and sanitizing the values.
2158
          $form['product_id'] = array(
2159
            '#type' => 'select',
2160
            '#options' => $options,
2161
            '#default_value' => $default_product->product_id,
2162
            '#weight' => 0,
2163
            '#ajax' => array(
2164
              'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
2165
            ),
2166
          );
2167
        }
2168
      }
2169
    }
2170

    
2171
    // Render the quantity field as either a textfield if shown or a hidden
2172
    // field if not.
2173
    if ($show_quantity) {
2174
      $form['quantity'] = array(
2175
        '#type' => 'textfield',
2176
        '#title' => t('Quantity'),
2177
        '#default_value' => $default_quantity,
2178
        '#datatype' => 'integer',
2179
        '#size' => 5,
2180
        '#weight' => 45,
2181
      );
2182
    }
2183
    else {
2184
      $form['quantity'] = array(
2185
        '#type' => 'hidden',
2186
        '#value' => $default_quantity,
2187
        '#datatype' => 'integer',
2188
        '#weight' => 45,
2189
      );
2190
    }
2191

    
2192
    // Add the line item's fields to a container on the form.
2193
    $form['line_item_fields'] = array(
2194
      '#type' => 'container',
2195
      '#parents' => array('line_item_fields'),
2196
      '#weight' => 10,
2197
      '#tree' => TRUE,
2198
    );
2199

    
2200
    field_attach_form('commerce_line_item', $form_state['line_item'], $form['line_item_fields'], $form_state);
2201

    
2202
    // Loop over the fields we just added and remove any that haven't been
2203
    // marked for inclusion on this form.
2204
    foreach (element_children($form['line_item_fields']) as $field_name) {
2205
      $info = field_info_instance('commerce_line_item', $field_name, $form_state['line_item']->type);
2206
      $form['line_item_fields'][$field_name]['#commerce_cart_settings'] = commerce_cart_field_instance_access_settings($info);
2207

    
2208
      if (empty($form['line_item_fields'][$field_name]['#commerce_cart_settings']['field_access'])) {
2209
        $form['line_item_fields'][$field_name]['#access'] = FALSE;
2210
      }
2211
    }
2212

    
2213
    // Do not allow products without a price to be purchased.
2214
    $values = commerce_product_calculate_sell_price($form_state['default_product']);
2215

    
2216
    if (is_null($values) || is_null($values['amount']) || is_null($values['currency_code'])) {
2217
      $form['submit'] = array(
2218
        '#type' => 'submit',
2219
        '#value' => t('Product not available'),
2220
        '#weight' => 50,
2221
        // Do not set #disabled in order not to prevent submission.
2222
        '#attributes' => array('disabled' => 'disabled'),
2223
        '#validate' => array('commerce_cart_add_to_cart_form_disabled_validate'),
2224
      );
2225
    }
2226
    else {
2227
      $form['submit'] = array(
2228
        '#type' => 'submit',
2229
        '#value' => t('Add to cart'),
2230
        '#weight' => 50,
2231
      );
2232
    }
2233
  }
2234

    
2235
  // Add the handlers manually since we're using hook_forms() to associate this
2236
  // form with form IDs based on the $product_ids.
2237
  $form['#validate'][] = 'commerce_cart_add_to_cart_form_validate';
2238
  $form['#submit'][] = 'commerce_cart_add_to_cart_form_submit';
2239

    
2240
  return $form;
2241
}
2242

    
2243
/**
2244
 * Validation callback that prevents submission if the product is not available.
2245
 */
2246
function commerce_cart_add_to_cart_form_disabled_validate($form, &$form_state) {
2247
  form_set_error('submit', t('This product is no longer available.'));
2248
}
2249

    
2250
/**
2251
 * Form validate handler: validate the product and quantity to add to the cart.
2252
 */
2253
function commerce_cart_add_to_cart_form_validate($form, &$form_state) {
2254
  // Check to ensure the quantity is valid.
2255
  if (!is_numeric($form_state['values']['quantity']) || $form_state['values']['quantity'] <= 0) {
2256
    form_set_error('quantity', t('You must specify a valid quantity to add to the cart.'));
2257
  }
2258

    
2259
  // If the custom data type attribute of the quantity element is integer,
2260
  // ensure we only accept whole number values.
2261
  if ($form['quantity']['#datatype'] == 'integer' &&
2262
    (int) $form_state['values']['quantity'] != $form_state['values']['quantity']) {
2263
    form_set_error('quantity', t('You must specify a whole number for the quantity.'));
2264
  }
2265

    
2266
  // If the attributes matching product selector was used, set the value of the
2267
  // product_id field to match; this will be fixed on rebuild when the actual
2268
  // default product will be selected based on the product selector value.
2269
  if (!empty($form_state['values']['attributes']['product_select'])) {
2270
    form_set_value($form['product_id'], $form_state['values']['attributes']['product_select'], $form_state);
2271
  }
2272

    
2273
  // Validate any line item fields that may have been included on the form.
2274
  field_attach_form_validate('commerce_line_item', $form_state['line_item'], $form['line_item_fields'], $form_state);
2275
}
2276

    
2277
/**
2278
 * Ajax callback: returns AJAX commands when an attribute widget is changed.
2279
 */
2280
function commerce_cart_add_to_cart_form_attributes_refresh($form, $form_state) {
2281
  $commands = array();
2282

    
2283
  // Render the form afresh to capture any changes to the available widgets
2284
  // based on the latest selection.
2285
  $commands[] = ajax_command_replace('.' . drupal_html_class($form['#form_id']), drupal_render($form));
2286

    
2287
  // Then render and return the various product fields that might need to be
2288
  // updated on the page.
2289
  if (!empty($form_state['context'])) {
2290
    $product = $form_state['default_product'];
2291
    $product->display_context = $form_state['context'];
2292

    
2293
    // First render the actual fields attached to the referenced product.
2294
    foreach (field_info_instances('commerce_product', $product->type) as $product_field_name => $product_field) {
2295
      // Rebuild the same array of classes used when the field was first rendered.
2296
      $replacement_class = drupal_html_class(implode('-', array($form_state['context']['class_prefix'], 'product', $product_field_name)));
2297

    
2298
      $classes = array(
2299
        'commerce-product-field',
2300
        drupal_html_class('commerce-product-field-' . $product_field_name),
2301
        drupal_html_class('field-' . $product_field_name),
2302
        $replacement_class,
2303
      );
2304

    
2305
      $element = field_view_field('commerce_product', $product, $product_field_name, $form_state['context']['view_mode']);
2306

    
2307
      // Add an extra class to distinguish empty product fields.
2308
      if (empty($element)) {
2309
        $classes[] = 'commerce-product-field-empty';
2310
      }
2311

    
2312
      // Append the prefix and suffix around existing values if necessary.
2313
      $element += array('#prefix' => '', '#suffix' => '');
2314
      $element['#prefix'] = '<div class="' . implode(' ', $classes) . '">' . $element['#prefix'];
2315
      $element['#suffix'] .= '</div>';
2316

    
2317
      $commands[] = ajax_command_replace('.' . $replacement_class, drupal_render($element));
2318
    }
2319

    
2320
    // Then render the extra fields defined for the referenced product.
2321
    foreach (field_info_extra_fields('commerce_product', $product->type, 'display') as $product_extra_field_name => $product_extra_field) {
2322
      $display = field_extra_fields_get_display('commerce_product', $product->type, $form_state['context']['view_mode']);
2323

    
2324
      // Only include extra fields that specify a theme function and that
2325
      // are visible on the current view mode.
2326
      if (!empty($product_extra_field['theme']) &&
2327
        !empty($display[$product_extra_field_name]['visible'])) {
2328
        // Rebuild the same array of classes used when the field was first rendered.
2329
        $replacement_class = drupal_html_class(implode('-', array($form_state['context']['class_prefix'], 'product', $product_extra_field_name)));
2330

    
2331
        $classes = array(
2332
          'commerce-product-extra-field',
2333
          drupal_html_class('commerce-product-extra-field-' . $product_extra_field_name),
2334
          $replacement_class,
2335
        );
2336

    
2337
        // Theme the product extra field to $element.
2338
        $variables = array(
2339
          $product_extra_field_name => $product->{$product_extra_field_name},
2340
          'label' => $product_extra_field['label'] . ':',
2341
          'product' => $product,
2342
        );
2343

    
2344
        $element = array(
2345
          '#markup' => theme($product_extra_field['theme'], $variables),
2346
          '#attached' => array(
2347
            'css' => array(drupal_get_path('module', 'commerce_product') . '/theme/commerce_product.theme.css'),
2348
          ),
2349
          '#prefix' => '<div class="' . implode(' ', $classes) . '">',
2350
          '#suffix' => '</div>',
2351
        );
2352

    
2353
        // Add an extra class to distinguish empty fields.
2354
        if (empty($element['#markup'])) {
2355
          $classes[] = 'commerce-product-extra-field-empty';
2356
        }
2357

    
2358
        $commands[] = ajax_command_replace('.' . $replacement_class, drupal_render($element));
2359
      }
2360
    }
2361
  }
2362

    
2363
  // Allow other modules to add arbitrary AJAX commands on the refresh.
2364
  drupal_alter('commerce_cart_attributes_refresh', $commands, $form, $form_state);
2365

    
2366
  return array('#type' => 'ajax', '#commands' => $commands);
2367
}
2368

    
2369
/**
2370
 * Form submit handler: add the selected product to the cart.
2371
 */
2372
function commerce_cart_add_to_cart_form_submit($form, &$form_state) {
2373
  $product_id = $form_state['values']['product_id'];
2374
  $product = commerce_product_load($product_id);
2375

    
2376
  // If the line item passed to the function is new...
2377
  if (empty($form_state['line_item']->line_item_id)) {
2378
    // Create the new product line item of the same type.
2379
    $line_item = commerce_product_line_item_new($product, $form_state['values']['quantity'], 0, $form_state['line_item']->data, $form_state['line_item']->type);
2380

    
2381
    // Allow modules to prepare this as necessary. This hook is defined by the
2382
    // Product Pricing module.
2383
    drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item);
2384

    
2385
    // Remove line item field values the user didn't have access to modify.
2386
    foreach ($form_state['values']['line_item_fields'] as $field_name => $value) {
2387
      // Note that we're checking the Commerce Cart settings that we inserted
2388
      // into this form element array back when we built the form. This means a
2389
      // module wanting to alter a line item field widget to be available must
2390
      // update both its form element's #access value and the field_access value
2391
      // of the #commerce_cart_settings array.
2392
      if (empty($form['line_item_fields'][$field_name]['#commerce_cart_settings']['field_access'])) {
2393
        unset($form_state['values']['line_item_fields'][$field_name]);
2394
      }
2395
    }
2396

    
2397
    // Unset the line item field values array if it is now empty.
2398
    if (empty($form_state['values']['line_item_fields'])) {
2399
      unset($form_state['values']['line_item_fields']);
2400
    }
2401

    
2402
    // Add field data to the line item.
2403
    field_attach_submit('commerce_line_item', $line_item, $form['line_item_fields'], $form_state);
2404

    
2405
    // Process the unit price through Rules so it reflects the user's actual
2406
    // purchase price.
2407
    rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
2408

    
2409
    // Only attempt an Add to Cart if the line item has a valid unit price.
2410
    $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
2411

    
2412
    if (!is_null($line_item_wrapper->commerce_unit_price->value())) {
2413
      // Add the product to the specified shopping cart.
2414
      $form_state['line_item'] = commerce_cart_product_add(
2415
        $form_state['values']['uid'],
2416
        $line_item,
2417
        isset($line_item->data['context']['add_to_cart_combine']) ? $line_item->data['context']['add_to_cart_combine'] : TRUE
2418
      );
2419
    }
2420
    else {
2421
      drupal_set_message(t('%title could not be added to your cart.', array('%title' => $product->title)), 'error');
2422
    }
2423
  }
2424
}
2425

    
2426
/**
2427
 * Implements hook_field_info_alter().
2428
 */
2429
function commerce_cart_field_info_alter(&$info) {
2430
  // Set the default display formatter for product reference fields to the Add
2431
  // to Cart form.
2432
  $info['commerce_product_reference']['default_formatter'] = 'commerce_cart_add_to_cart_form';
2433
}
2434

    
2435
/**
2436
 * Implements hook_field_formatter_info().
2437
 */
2438
function commerce_cart_field_formatter_info() {
2439
  return array(
2440
    'commerce_cart_add_to_cart_form' => array(
2441
      'label' => t('Add to Cart form'),
2442
      'description' => t('Display an Add to Cart form for the referenced product.'),
2443
      'field types' => array('commerce_product_reference', 'entityreference'),
2444
      'settings' => array(
2445
        'show_quantity' => FALSE,
2446
        'default_quantity' => 1,
2447
        'combine' => TRUE,
2448
        'show_single_product_attributes' => FALSE,
2449
        'line_item_type' => 'product',
2450
      ),
2451
    ),
2452
  );
2453
}
2454

    
2455
/**
2456
 * Implements hook_field_formatter_settings_form().
2457
 */
2458
function commerce_cart_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
2459
  $display = $instance['display'][$view_mode];
2460
  $settings = array_merge(field_info_formatter_settings($display['type']), $display['settings']);
2461

    
2462
  $element = array();
2463

    
2464
  if ($display['type'] == 'commerce_cart_add_to_cart_form') {
2465
    $element['show_quantity'] = array(
2466
      '#type' => 'checkbox',
2467
      '#title' => t('Display a textfield quantity widget on the add to cart form.'),
2468
      '#default_value' => $settings['show_quantity'],
2469
    );
2470

    
2471
    $element['default_quantity'] = array(
2472
      '#type' => 'textfield',
2473
      '#title' => t('Default quantity'),
2474
      '#default_value' => $settings['default_quantity'] <= 0 ? 1 : $settings['default_quantity'],
2475
      '#element_validate' => array('commerce_cart_field_formatter_settings_form_quantity_validate'),
2476
      '#size' => 16,
2477
    );
2478

    
2479
    $element['combine'] = array(
2480
      '#type' => 'checkbox',
2481
      '#title' => t('Attempt to combine like products on the same line item in the cart.'),
2482
      '#description' => t('The line item type, referenced product, and data from fields exposed on the Add to Cart form must all match to combine.'),
2483
      '#default_value' => $settings['combine'],
2484
    );
2485

    
2486
    $element['show_single_product_attributes'] = array(
2487
      '#type' => 'checkbox',
2488
      '#title' => t('Show attribute widgets even if the Add to Cart form only represents one product.'),
2489
      '#description' => t('If enabled, attribute widgets will be shown on the form with the only available options selected.'),
2490
      '#default_value' => $settings['show_single_product_attributes'],
2491
    );
2492

    
2493
    // Add a conditionally visible line item type element.
2494
    $types = commerce_product_line_item_types();
2495

    
2496
    if (count($types) > 1) {
2497
      $element['line_item_type'] = array(
2498
        '#type' => 'select',
2499
        '#title' => t('Add to Cart line item type'),
2500
        '#options' => array_intersect_key(commerce_line_item_type_get_name(), drupal_map_assoc($types)),
2501
        '#default_value' => $settings['line_item_type'],
2502
      );
2503
    }
2504
    else {
2505
      $element['line_item_type'] = array(
2506
        '#type' => 'hidden',
2507
        '#value' => reset($types),
2508
      );
2509
    }
2510
  }
2511

    
2512
  return $element;
2513
}
2514

    
2515
/**
2516
 * Element validate callback: ensure a valid quantity is entered.
2517
 */
2518
function commerce_cart_field_formatter_settings_form_quantity_validate($element, &$form_state, $form) {
2519
  if (!is_numeric($element['#value']) || $element['#value'] <= 0) {
2520
    form_set_error(implode('][', $element['#parents']), t('You must enter a positive numeric default quantity value.'));
2521
  }
2522
}
2523

    
2524
/**
2525
 * Implements hook_field_formatter_settings_summary().
2526
 */
2527
function commerce_cart_field_formatter_settings_summary($field, $instance, $view_mode) {
2528
  $display = $instance['display'][$view_mode];
2529
  $settings = array_merge(field_info_formatter_settings($display['type']), $display['settings']);
2530

    
2531
  $summary = array();
2532

    
2533
  if ($display['type'] == 'commerce_cart_add_to_cart_form') {
2534
    $summary = array(
2535
      t('Quantity widget: !status', array('!status' => !empty($settings['show_quantity']) ? t('Enabled') : t('Disabled'))),
2536
      t('Default quantity: @quantity', array('@quantity' => $settings['default_quantity'])),
2537
      t('Combine like items: !status', array('!status' => !empty($settings['combine']) ? t('Enabled') : t('Disabled'))),
2538
      t('!visibility attributes on single product forms.', array('!visibility' => !empty($settings['show_single_product_attributes']) ? t('Showing') : t('Hiding'))),
2539
    );
2540

    
2541
    if (count(commerce_product_line_item_types()) > 1) {
2542
      $summary[] = t('Add to Cart line item type: @type', array('@type' => commerce_line_item_type_get_name($settings['line_item_type'])));
2543
    }
2544
  }
2545

    
2546
  return implode('<br />', $summary);
2547
}
2548

    
2549
/**
2550
 * Implements hook_field_formatter_view().
2551
 */
2552
function commerce_cart_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
2553
  $settings = array_merge(field_info_formatter_settings($display['type']), $display['settings']);
2554
  $result = array();
2555

    
2556
  // Collect the list of product IDs.
2557
  $product_ids = array();
2558

    
2559
  foreach ($items as $delta => $item) {
2560
    if (isset($item['product_id'])) {
2561
      $product_ids[] = $item['product_id'];
2562
    }
2563
    elseif (module_exists('entityreference') && isset($item['target_id'])) {
2564
      $product_ids[] = $item['target_id'];
2565
    }
2566
  }
2567

    
2568
  if ($display['type'] == 'commerce_cart_add_to_cart_form') {
2569
    // Load the referenced products.
2570
    $products = commerce_product_load_multiple($product_ids);
2571

    
2572
    // Check to ensure products are referenced, before returning results.
2573
    if (!empty($products)) {
2574
      $type = !empty($settings['line_item_type']) ? $settings['line_item_type'] : 'product';
2575
      $line_item = commerce_product_line_item_new(commerce_product_reference_default_product($products), $settings['default_quantity'], 0, array(), $type);
2576
      $line_item->data['context']['product_ids'] = array_keys($products);
2577
      $line_item->data['context']['add_to_cart_combine'] = !empty($settings['combine']);
2578
      $line_item->data['context']['show_single_product_attributes'] = !empty($settings['show_single_product_attributes']);
2579

    
2580
      $result[] = array(
2581
        '#arguments' => array(
2582
          'form_id' => commerce_cart_add_to_cart_form_id($product_ids),
2583
          'line_item' => $line_item,
2584
          'show_quantity' => $settings['show_quantity'],
2585
        ),
2586
      );
2587
    }
2588
  }
2589

    
2590
  return $result;
2591
}
2592

    
2593
/**
2594
 * Implements hook_field_attach_view_alter().
2595
 *
2596
 * When a field is formatted for display, the display formatter does not know
2597
 * what view mode it is being displayed for. Unfortunately, the Add to Cart form
2598
 * display formatter needs this information when displaying product reference
2599
 * fields on nodes to provide adequate context for product field replacement on
2600
 * multi-value product reference fields. This hook is used to transform a set of
2601
 * arguments into a form using the arguments and the extra context information
2602
 * gleaned from the parameters passed into this function.
2603
 */
2604
function commerce_cart_field_attach_view_alter(&$output, $context) {
2605
  // Loop through the fields passed in looking for any product reference fields
2606
  // formatted with the Add to Cart form display formatter.
2607
  foreach ($output as $field_name => $element) {
2608
    if (!empty($element['#formatter']) && $element['#formatter'] == 'commerce_cart_add_to_cart_form') {
2609
      // Prepare the context information needed by the cart form.
2610
      $cart_context = $context;
2611

    
2612
      // Remove the full entity from the context array and put the ID in instead.
2613
      list($entity_id, $vid, $bundle) = entity_extract_ids($context['entity_type'], $context['entity']);
2614
      $cart_context['entity_id'] = $entity_id;
2615
      unset($cart_context['entity']);
2616

    
2617
      // Remove any Views data added to the context by views_handler_field_field.
2618
      // It unnecessarily increases the size of rows in the cache_form table for
2619
      // Add to Cart form state data.
2620
      if (!empty($cart_context['display']) && is_array($cart_context['display'])) {
2621
        unset($cart_context['display']['views_view']);
2622
        unset($cart_context['display']['views_field']);
2623
        unset($cart_context['display']['views_row_id']);
2624
      }
2625

    
2626
      // Add the context for displaying product fields in the context of an entity
2627
      // that references the product by looking at the entity this product
2628
      // reference field is attached to.
2629
      $cart_context['class_prefix'] = $context['entity_type'] . '-' . $entity_id;
2630
      $cart_context['view_mode'] = $context['entity_type'] . '_' . $element['#view_mode'];
2631

    
2632
      $entity_uri = entity_uri($context['entity_type'], $element['#object']);
2633

    
2634
      foreach (element_children($element) as $key) {
2635
        // Extract the drupal_get_form() arguments array from the element.
2636
        $arguments = $element[$key]['#arguments'];
2637

    
2638
        // Add the display path and referencing entity data to the line item.
2639
        if (!empty($entity_uri['path'])) {
2640
          $arguments['line_item']->data['context']['display_path'] = $entity_uri['path'];
2641
        }
2642

    
2643
        $arguments['line_item']->data['context']['entity'] = array(
2644
          'entity_type' => $context['entity_type'],
2645
          'entity_id' => $entity_id,
2646
          'product_reference_field_name' => $field_name,
2647
        );
2648

    
2649
        // Update the product_ids variable to point to the entity data if we're
2650
        // referencing multiple products.
2651
        if (count($arguments['line_item']->data['context']['product_ids']) > 1) {
2652
          $arguments['line_item']->data['context']['product_ids'] = 'entity';
2653
        }
2654

    
2655
        // Replace the array containing the arguments with the return value of
2656
        // drupal_get_form(). It will be rendered when the rest of the object is
2657
        // rendered for display.
2658
        $output[$field_name][$key] = drupal_get_form($arguments['form_id'], $arguments['line_item'], $arguments['show_quantity'], $cart_context);
2659
      }
2660
    }
2661
  }
2662
}
2663

    
2664
/**
2665
 * Returns an array of product IDs used for building an Add to Cart form from
2666
 * the context information in a line item's data array.
2667
 *
2668
 * @param $line_item
2669
 *   The line item whose data array includes a context array used for building
2670
 *   an Add to Cart form.
2671
 *
2672
 * @return
2673
 *   The array of product IDs extracted from the line item.
2674
 *
2675
 * @see commerce_cart_add_to_cart_form()
2676
 */
2677
function commerce_cart_add_to_cart_form_product_ids($line_item) {
2678
  $product_ids = array();
2679

    
2680
  if (empty($line_item->data['context']) ||
2681
    empty($line_item->data['context']['product_ids']) ||
2682
    ($line_item->data['context']['product_ids'] == 'entity' && empty($line_item->data['context']['entity']))) {
2683
    return $product_ids;
2684
  }
2685

    
2686
  // If the product IDs setting tells us to use entity values...
2687
  if ($line_item->data['context']['product_ids'] == 'entity' &&
2688
    is_array($line_item->data['context']['entity'])) {
2689
    $entity_data = $line_item->data['context']['entity'];
2690

    
2691
    // Load the specified entity.
2692
    $entity = entity_load_single($entity_data['entity_type'], $entity_data['entity_id']);
2693

    
2694
    // Extract the product IDs from the specified product reference field.
2695
    if (!empty($entity->{$entity_data['product_reference_field_name']})) {
2696
      $product_ids = entity_metadata_wrapper($entity_data['entity_type'], $entity)->{$entity_data['product_reference_field_name']}->raw();
2697
    }
2698
  }
2699
  elseif (is_array($line_item->data['context']['product_ids'])) {
2700
    $product_ids = $line_item->data['context']['product_ids'];
2701
  }
2702

    
2703
  return $product_ids;
2704
}
2705

    
2706
/**
2707
 * Implements hook_preprocess_views_view().
2708
 */
2709
function commerce_cart_preprocess_views_view(&$vars) {
2710
  $view = $vars['view'];
2711

    
2712
  // Add the shopping cart stylesheet to the cart or form if they are not empty.
2713
  if ($view->name == 'commerce_cart_block' || $view->name == 'commerce_cart_form') {
2714
    drupal_add_css(drupal_get_path('module', 'commerce_cart') . '/theme/commerce_cart.theme.css');
2715
  }
2716
}
2717

    
2718
/**
2719
 * Implements hook_i18n_string_list_TEXTGROUP_alter().
2720
 */
2721
function commerce_cart_i18n_string_list_field_alter(&$strings, $type = NULL, $object = NULL) {
2722
  if (!isset($strings['field']) || !is_array($object) || !commerce_cart_field_instance_is_attribute($object)) {
2723
    return;
2724
  }
2725
  if (!empty($object['commerce_cart_settings']['attribute_widget_title'])) {
2726
    $strings['field'][$object['field_name']][$object['bundle']]['attribute_widget_title'] = array(
2727
      'string' => $object['commerce_cart_settings']['attribute_widget_title'],
2728
      'title' => t('Attribute widget title'),
2729
    );
2730
  }
2731
}