Creating Drupal Commerce add to cart links

By shane
Wed, 2014-06-25 07:29
20 comments

Share with Others

I recently ran across a problem of needing multiple Drupal Commerce add to cart buttons on a single page. Instead of trying to add a second add to cart form, I decided it would be easiest to create a custom link that could then be used to add products to the cart. This is useful if you need to create multiple add to cart links on the same page, or if you just need to create custom add to cart links to place in other areas of your Drupal Commerce website.

Updated Solution

So after trying all of these solutions, I found out that none of the solutions worked as well as I would like. Because of this, I found a secure, reliable (so far) solution. I will leave the original post below for reference, but here is how I would suggest adding an add to cart button. The solution uses tokens to allow you to add a token to the body field of a node. This token will be replaced with an add to cart form when the user views the page. This will allow you to have multiple add to cart forms for the same product, on the same page. This add to cart form is often just an add to cart button (unless you use a quantity field, or have multiple product references for a single product display node). This means you are essentially able to add as many add to cart buttons as you want on a single page. This is useful for longer sales/landing pages with a lot of content. You may want an add to cart button in the middle, and one again at the bottom of the page.

I created a Sandbox project on Drupal.org with the module I outline below. Go ahead and give it a try and let me know what you think If it seems to work well for other people I will get it pushed into an official Drupal.org project.

https://www.drupal.org/sandbox/smthomas/2320659

Building the Add to Cart Form Token functionality

The first step was to download, install, and configure the Token Filter module. This will allow you to use tokens on your body field.

Next you will need to create a custom module with the following code in the .module file (replace MYMODULE with the name of your module):

/**
 * Implements hook_token_info().
 */
function MYMODULE_token_info() {
  $type = array(
    'name' => t('Add to Cart form'),
    'description' => t('Tokens to create an add to cart form.'),
  );
 
  return array(
    'types' => array('addtocartform' => $type),
  );
 
  return $info;
}
 
/**
 * Implements hook_tokens().
 */
function MYMODULE_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  if ($type == 'addtocartform') {
    foreach ($tokens as $name => $original) {
      list($product_id, $quantity) = explode(':', $name);
      $product = commerce_product_load($product_id);
      $line_item = commerce_product_line_item_new($product, $quantity);
      $line_item->data['context']['product_ids'] = array($product_id);
      $form = drupal_get_form('commerce_cart_add_to_cart_form', $line_item);
      $replacements[$original] = drupal_render($form);
    }
  }
  return $replacements;
}

So what does this do? Well you can add the following token to your body field now and the token will be replaced with an add to cart form:

[addtocartform:23:1]

This will create an add to cart form that adds a Quantity of 1 of Product ID 23 to your cart when clicked. If you want to add 2 of product id 55, your token would be:

[addtocartform:55:2]

Note: I had to create a new Full HTML text format that didn't have the automatic line breaks in order to get the Add to cart button to display correctly. You will need to make sure you allow full HTML in any body field that uses the addtocartform token.

------------------------------------------------------
---------------------------------------------------------
-----------------original post-------------------------
---------------------------------------------------------------
Here is the original post with some other alternatives/ideas on how to accomplish this same thing:

I broke this down into 3 different sections. The first goes over the original solution, the second covers a more complicated but more secure option, and the last lists an alternative (no code) solution.

Creating Multiple Add to Cart Links

WARNING: Be aware that this approach introduces possible CSRF vulnerabilities as the link can be used to add any product to your cart. I kept this posted as there are some who may be OK with this risk. You have been warned!

To do this I used a custom Drupal 7 module and started by implementing hook_menu().

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/%'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access checkout'),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2),
  );
 
}

The next step was to implement the MYMODULE_add_to_cart function. After doing a little research, I found the commerce_cart_product_add_by_id function.

This function needs to accept the product_id that is being passed from the hook_menu function, will call the commerce_cart_product_add_by_id function, and will then redirect the user to the cart page.

/**
 * Programmatically adds an item to a cart by product id.
 */
function MYMODULE_add_to_cart($product_id) {
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product_id);
 
  // Go to the cart page.
  drupal_goto('cart');
}

Now using a link such as:

<a href="/commerce/add-to-cart/1">Add to Cart</a>

I could add Product ID 1 to my cart. This would now work for any product id (just replace the number 1 with the correct product id number). Any link to commerce/add-to-cart/[product-id] will now work across my entire Drupal site.

This worked great for my needs, however it got me thinking of other possible use cases. What if we didn't know or have access to the product id (maybe you have a view that lists product nodes and you don't want to add the extra relationships). You could make this function work by node id instead of product id by changing the code to:

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/%node'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access checkout'),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2),
  );
}
 
/**
 * Programmatically adds an item to a cart by node id.
 */
function MYMODULE_add_to_cart($node) {
  // Get the product reference from the node.
  $product = field_get_items('node', $node, 'field_product_reference');
 
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product[0]['product_id']);
 
  // Go to the cart page.
  drupal_goto('cart');
}

Keep in mind the first way is the more preferable option. Also keep in mind this will only work if the node to product reference is one to one. If you have multiple products being referenced from the product display node, this will only add the first product referenced.

You may also want to be able to specify the quantity from the link, this is also very easy to add. The commerce_cart_product_add_by_id function accepts a second parameter for the quantity. Making the change to the code is pretty simple:

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/%'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access checkout'),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2, 3),
  );
  return $items;
}
 
/**
 * Programmatically adds an item to a cart by product id.
 */
function MYMODULE_add_to_cart($product_id, $quantity = 1) {
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product_id, $quantity);
 
  // Go to the cart page.
  drupal_goto('cart');
}

Notice how the only changes needed were to add an additional page argument to the menu item, and then add that to the commerce_cart_product_add_by_id function call. I could have added an additional wildcard character (%) to the end of the menu item making it commerce/add-to-cart/%/% but that would have made the quantity required. This way the quantity value is optional and will default to 1 if left out.

You could now use all of the following links.

To add a quantity of 5 of Product ID 1:

<a href="/commerce/add-to-cart/1/5">Add to Cart</a>

To add a quantity of 1 of Product ID 23 (it will default the quantity to 1 if you leave it out):

<a href="/commerce/add-to-cart/23">Add to Cart</a>

Note: I am using the first example (using the product id not the node id option) since that is the most preferable implementation.

More secure Add to Cart Links

This approach is similar to the approach listed above, but slightly more complicated. In order to secure from a CSRF vulnerability, there needs to be a dynamically generated token on the end of the URL. This however makes adding a link in the content of a Node difficult (I needed the link to be in the body field on a content type). I came up with a token based solution to get this to work.

The first step was to download, install, and configure the Token Filter module. This will allow you to use tokens on your body field.

The next step is to create a custom module, or add code to an existing custom module. This code is similar to the code above, but has a few important differences.

The first big difference is that the menu item has an access callback. This access callback verifies that the link that was clicked has a correct token value added to the end of the link. The second big change is the addition of an addtocart token type (read below the module code for examples of what this does):

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/%'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access callback' => 'MYMODULE_add_to_cart_access',
    'access arguments' => array(2),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2),
  );
 
  return $items;
}
 
/**
 * Access callback to verify user has correct permissions and token.
 */
function MYMODULE_add_to_cart_access($product_id, $quantity = 1) {
  $token = empty($_GET['token'])?"":$_GET['token'];
  $valid = drupal_valid_token($token, 'addtocart' . $product_id . $quantity);
 
  // If the token is valid and the user can access checkout, the user has access.
  if ($valid && user_access('access checkout')) {
    return TRUE;
  }
 
  return FALSE;
}
 
/**
 * Programmatically adds an item to a cart by product id.
 */
function MYMODULE_add_to_cart($product_id, $quantity = 1) {
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product_id, $quantity);
 
  // Go to the cart page.
  drupal_goto('cart');
}
 
/**
 * Implements hook_token_info().
 */
function MYMODULE_token_info() {
  $type = array(
    'name' => t('Add To Cart'),
    'description' => t('Tokens to add items to a users cart.'),
  );
 
  return array(
    'types' => array('addtocart' => $type),
  );
 
  return $info;
}
 
/**
 * Implements hook_tokens().
 */
function MYMODULE_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  if ($type == 'addtocart') {
    foreach ($tokens as $name => $original) {
      list($product_id, $quantity) = explode(':', $name);
      $token = drupal_get_token('addtocart' . $product_id . $quantity);
      $replacements[$original] = l('Add to Cart', 'commerce/add-to-cart/' . $product_id . '/' . $quantity, array('query' => array('token' => $token)));
    }
  }
  return $replacements;
}

So what does this do? Well you can add the following token to your body field now and the token will be replaced with a safer more secure add to cart link:

[addtocart:23:1]

This will create a link that adds a Quantity of 1 of Product ID 23 to your cart when clicked. If you want to add 2 of product id 55, your token would be:

[addtocart:55:2]

The link will be created for you when your page is rendered.

Note: This has been mostly untested besides a few basic tests. There could be issues with this that I haven't run into yet. If you do test it and have feedback, please let me know.

Using Rules Link as an alternative

It was also mentioned in the comments that the Rules Link module could be used to accomplish almost the same thing. I have not used this module so I don't know exactly how it works, but it appears that it would work. You could build the functionality as a Rule and then execute that rule by using a Rules Link. I will try to take a look at this module further and report back how this would need to be set up.

How else could this be used? Do you have any questions on ways to modify this to work for you? Let me know in the comments.

Comments

As it is, your code has a CSRF vulnerability. You need to add a token parameter to the links, generated with something like drupal_get_token($product_id, $quantity), and validate it with drupal_valid_token().

I updated the post with an alternative solution using the method you described. It was a little more complicated, but it seems to be working in my limited tests. Thanks for the comment!

Thanks for making the update - sorry to get you to do that much more work! I suppose that's how complex CSRF protection is in general, and why the Commerce maintainers chose to use the form API.

I think Ryan's suggestion of Rules Link might be worth investigating - but although it might be quite easy to add the link to the node, it might be too much work to insert links at arbitrary points inside the Body field.

(BTW in my comment above I have a comma where I should have a . operator, sorry)

No worries on the extra work. It was a good learning experience and it has been some time since I played around with custom tokens.

I think I will eventually wrap this up as a module and attach it to this post so others can use it as an example if they need to create CSRF safe links inside body fields... although it seems like a pretty small use case :-)

Thanks for the help!

Good utilization of the API! Did you consider using Rules Links at all and configuring this similarly through the Rules action to add a product to the cart? It isn't as robust as using the API directly, but it may be a quick & dirty alternative for a non-developer.

I updated the post to let people know about the Rules Link module. I haven't used it but it does seem that it would work. Thanks for the idea!

The method I posted had a few problems that didn't allow it to work very well with SSL and caching. Because of this, I decided to try a new approach (you can read about it up above). I also decided to package this up as a small little module and put it on Drupal.org in my sandbox.

https://www.drupal.org/sandbox/smthomas/2320659

You can check it out with Git and try it out. If there is some demand for it and it works pretty well, I will get it published into an official Drupal.org project.

Let me know if you have a chance to take a look at it.

Thanks for the link to the Commerce Express Checkout module. I will take a look at it as it seems to do much of the same things that I tried to do in this tutorial.

Just needed to do this, as client didn't want users to have to scroll back up after reading the whole product description.
I just added a new product reference field to the product display, identical to the other one, and set it to appear at the bottom. Seems to work fine? Am I missing some problem with this?

I think this solution would work, but if you needed to have 3 links now you have three product reference fields. This might cause a lot of extra work when adding new products as you have to reference the product multiple times. This also means that all product displays would need to be identical and you would not be able to have extra links for those products with longer descriptions.

Either way, this sounds like a great solution in most cases and I am glad it worked for you. It is certainly a lot easier than the solutions I outlined above :-)

Thanks for the comment.

My client only has one product currently, so this solution was fine for them. Obviously if there were lots of products or variations I would need a more robust solution. But, for those who just need a quick fix, this is an easy win...

I am not sure exactly what you are asking, but if you click the link multiple times, it will add multiple quantities to the cart. Is that what you mean?

I think using the techniques in the article, you could make it work for what you need.

Thanks for the comment and perhaps you can elaborate on what you are looking to do.

Hi, I meant to be able to construct link like this addtocartform:23:1, 44:1,etc. Ability to add to cart multiple products at once. This is really needed feature - just simply add more products to the cart without any complicated setup or forms. Thank you.

I am not as CSRF expert, however here is why I think there is a CSRF vulnerability. There is a single URL that changes the users cart and session data. This means if the user were to go to any other site, this 2nd site could request the add to cart URL from the 1st site and add any product to the users cart on the 1st site.

The CSRF vulnerability in this example is not as serious as some other vulnerabilities, but it still allows an external website to change information of a user. Granted it only allows adding products to the users cart, it's still considered a CSRF vulnerability.

I am sure someone with more knowledge of CSRF and web security could explain it better, but thanks for you comment.

Thank you for this article, very useful! My product catalog was very HTML-heavy before implementing this.
P.S. You forgot to return items in MYMODULE_menu hook.

Post new comment