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 @ b858700c

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

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

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

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

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

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

    
63
  return $items;
64
}
65

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

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

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

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

    
88
  return $title;
89
}
90

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

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

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

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

    
152
  return $hooks;
153
}
154

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

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

    
169
  return $order_states;
170
}
171

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

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

    
185
  return $order_statuses;
186
}
187

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

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

    
202
  return $checkout_panes;
203
}
204

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
361
  // Add a fieldset for settings pertaining to the shopping cart refresh.
362
  $form['cart_refresh'] = array(
363
    '#type' => 'fieldset',
364
    '#title' => t('Shopping cart refresh'),
365
    '#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.'),
366
    '#weight' => 40,
367
  );
368
  $form['cart_refresh']['commerce_cart_refresh_mode'] = array(
369
    '#type' => 'radios',
370
    '#title' => t('Shopping cart refresh mode'),
371
    '#options' => array(
372
      COMMERCE_CART_REFRESH_ALWAYS => t('Refresh a shopping cart when it is loaded regardless of who it belongs to.'),
373
      COMMERCE_CART_REFRESH_OWNER_ONLY => t('Only refresh a shopping cart when it is loaded if it belongs to the current user.'),
374
      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."),
375
    ),
376
    '#default_value' => variable_get('commerce_cart_refresh_mode', COMMERCE_CART_REFRESH_OWNER_ONLY),
377
  );
378
  $form['cart_refresh']['commerce_cart_refresh_frequency'] = array(
379
    '#type' => 'textfield',
380
    '#title' => t('Shopping cart refresh frequency'),
381
    '#description' => t('Shopping carts will only be refreshed if more than the specified number of seconds have passed since they were last refreshed.'),
382
    '#default_value' => variable_get('commerce_cart_refresh_frequency', 30),
383
    '#required' => TRUE,
384
    '#size' => 32,
385
    '#field_suffix' => t('seconds'),
386
    '#element_validate' => array('commerce_cart_validate_refresh_frequency'),
387
  );
388
  $form['cart_refresh']['commerce_cart_refresh_force'] = array(
389
    '#type' => 'checkbox',
390
    '#title' => t('Always refresh shopping cart orders on shopping cart and checkout form pages regardless of other settings.'),
391
    '#description' => t('Note: this option only applies to the core /cart and /checkout/* paths.'),
392
    '#default_value' => variable_get('commerce_cart_refresh_force', TRUE),
393
  );
394
}
395

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

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

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

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

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

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

    
456
    $form['instance']['commerce_cart_settings'] = array(
457
      '#type' => 'fieldset',
458
      '#title' => t('Attribute field settings'),
459
      '#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.'),
460
      '#weight' => 5,
461
      '#collapsible' => FALSE,
462
    );
463
    $form['instance']['commerce_cart_settings']['attribute_field'] = array(
464
      '#type' => 'checkbox',
465
      '#title' => t('Enable this field to function as an attribute field on Add to Cart forms.'),
466
      '#default_value' => $commerce_cart_settings['attribute_field'],
467
    );
468
    $form['instance']['commerce_cart_settings']['attribute_widget'] = array(
469
      '#type' => 'radios',
470
      '#title' => t('Attribute selection widget'),
471
      '#description' => t('The type of element used to select an option if used on an Add to Cart form.'),
472
      '#options' => array(
473
        'select' => t('Select list'),
474
        'radios' => t('Radio buttons'),
475
      ),
476
      '#default_value' => $commerce_cart_settings['attribute_widget'],
477
      '#states' => array(
478
        'visible' => array(
479
          ':input[name="instance[commerce_cart_settings][attribute_field]"]' => array('checked' => TRUE),
480
        ),
481
      ),
482
    );
483

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

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

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

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

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

    
513
    $form['instance']['commerce_cart_settings'] = array(
514
      '#type' => 'fieldset',
515
      '#title' => t('Add to Cart form settings'),
516
      '#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.'),
517
      '#weight' => 5,
518
      '#collapsible' => FALSE,
519
    );
520
    $form['instance']['commerce_cart_settings']['field_access'] = array(
521
      '#type' => 'checkbox',
522
      '#title' => t('Include this field on Add to Cart forms for line items of this type.'),
523
      '#default_value' => $commerce_cart_settings['field_access'],
524
    );
525
  }
526
}
527

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

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

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

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

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

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

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

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

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

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

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

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

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

    
642
  return $blocks;
643
}
644

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
731
  return TRUE;
732
}
733

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

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

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

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

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

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

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

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

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

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

    
815
  return FALSE;
816
}
817

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
923
  return $order;
924
}
925

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

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

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

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

    
949
  return $is_cart;
950
}
951

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

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

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

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

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

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

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

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

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

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

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

    
1048
    return $order_wrapper;
1049
  }
1050

    
1051
  return FALSE;
1052
}
1053

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1152
  return $order_wrapper;
1153
}
1154

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1382
    commerce_line_item_save($matching_line_item);
1383

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

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

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

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

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

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

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

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

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

    
1445
  return FALSE;
1446
}
1447

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

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

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

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

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

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

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

    
1489
  return $order;
1490
}
1491

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

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

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

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

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

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

    
1524
  return $order;
1525
}
1526

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

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

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

    
1582
  return $commerce_cart_settings;
1583
}
1584

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

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

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

    
1632
  return $commerce_cart_settings;
1633
}
1634

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

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

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

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

    
1661
  return check_plain($title);
1662
}
1663

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

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

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

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

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

    
1698
  return $forms;
1699
}
1700

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2239
  return $form;
2240
}
2241

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2461
  $element = array();
2462

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

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

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

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

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

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

    
2511
  return $element;
2512
}
2513

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

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

    
2530
  $summary = array();
2531

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

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

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

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

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

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

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

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

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

    
2589
  return $result;
2590
}
2591

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2702
  return $product_ids;
2703
}
2704

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

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

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