<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/*
 * Exp:Store module for ExpressionEngine 2.x by Crescendo (support@crescendo.net.nz)
 * Copyright (c) 2010-2012 Crescendo Multimedia Ltd
 * All rights reserved.
 */

class Store {

	public function __construct()
	{
		$this->EE =& get_instance();

		$this->EE->lang->loadfile('store');
		$this->EE->load->model('store_model');
		$this->EE->load->helper(array('store', 'form'));
		$this->EE->load->library(array('store_cart', 'store_config', 'javascript'));

		// sometimes having a submit button named "submit" can cause JS issues
		// we provide "commit" as an alternative button name
		if (isset($_POST['commit'])) $_POST['submit'] = $_POST['commit'];
	}

	/*
	 * TEMPLATE TAGS
	 */

	public function product()
	{
		$this->_tmpl_secure_check(FALSE);

		$this->EE->load->model('store_products_model');

		$entry_id = (int)$this->EE->TMPL->fetch_param('entry_id');
		if ($entry_id == 0)
		{
			return '<p><strong>'.sprintf(lang('invalid_parameter'), 'entry_id').'</strong></p>';
		}

		$product = $this->EE->store_products_model->find_by_id($entry_id);
		if (empty($product)) return;

		$product = $this->EE->store_cart->process_product_tax($product);
		$tag_vars = array($product);
		$tag_vars[0]['modifiers'] = array();
		$tag_vars[0]['cart_updated'] = $this->EE->session->flashdata('store_cart_updated');
		$tag_vars[0]['min_order_qty'] = 1;

		foreach ($product['stock'] as $sku)
		{
			// update product min order qty
			if ($sku['min_order_qty'] > $tag_vars[0]['min_order_qty'])
			{
				$tag_vars[0]['min_order_qty'] = $sku['min_order_qty'];
			}

			// add sku details to modifiers
			// these variables are only useful if the product has one "variation" modifier
			// if the product has more than one variation group then each SKU doesn't align with
			// only one option, so these variables are not available
			if (count($sku['opt_values']) == 1)
			{
				$opt_id = reset($sku['opt_values']);
				$mod_id = key($sku['opt_values']);
				$option =& $product['modifiers'][$mod_id]['options'][$opt_id];
				$option['sku'] = $sku['sku'];
				$option['track_stock'] = $sku['track_stock'] == 'y';
				$option['stock_level'] = $option['track_stock'] ? $sku['stock_level'] : FALSE;
				$option['min_order_qty'] = $sku['min_order_qty'];
			}
		}

		// build product modifiers array
		foreach ($product['modifiers'] as $mod_data)
		{
			$new_mod = array(
				'modifier_id' => $mod_data['product_mod_id'],
				'modifier_name' => $mod_data['mod_name'],
				'modifier_input_name' => "modifiers[{$mod_data['product_mod_id']}]",
				'modifier_type' => $mod_data['mod_type'],
				'modifier_instructions' => $mod_data['mod_instructions'],
				'modifier_options' => array()
			);

			$select_options = array();

			foreach ($mod_data['options'] as $opt_data)
			{
				$new_opt = array(
					'option_id' => $opt_data['product_opt_id'],
					'option_name' => $opt_data['opt_name'],
					'option_first' => FALSE,
					'option_last' => FALSE,
					// these variables only appear if the product has one "var" type modifier
					'option_sku' => isset($opt_data['sku']) ? $opt_data['sku'] : FALSE,
					'option_track_stock' => isset($opt_data['track_stock']) ? $opt_data['track_stock'] : FALSE,
					'option_stock_level' => isset($opt_data['stock_level']) ? $opt_data['stock_level'] : FALSE,
					'option_min_order_qty' => isset($opt_data['min_order_qty']) ? $opt_data['min_order_qty'] : FALSE,
				);

				$new_opt['price_mod'] = $opt_data['opt_price_mod'];
				$new_opt['price_mod_val'] = $opt_data['opt_price_mod_val'];
				$new_opt['price_mod_inc_tax_val'] = store_round_currency($new_opt['price_mod_val'] * (1 + $this->EE->store_cart->tax_rate()), TRUE);
				$new_opt['price_mod_inc_tax'] = store_format_currency($new_opt['price_mod_inc_tax_val'], TRUE);
				$new_opt['price_inc_mod_val'] = store_round_currency($new_opt['price_mod_val'] + $product['price_val']);
				$new_opt['price_inc_mod'] = store_format_currency($new_opt['price_inc_mod_val']);
				$new_opt['price_inc_mod_inc_tax_val'] = store_round_currency($new_opt['price_inc_mod_val'] * (1 + $this->EE->store_cart->tax_rate()));
				$new_opt['price_inc_mod_inc_tax'] = store_format_currency($new_opt['price_inc_mod_inc_tax_val']);

				$new_mod['modifier_options'][] = $new_opt;
				$select_options[$opt_data['product_opt_id']] = $opt_data['opt_name'];
			}

			$modifier_options_count = count($new_mod['modifier_options']);
			if ($modifier_options_count > 0)
			{
				$new_mod['modifier_options'][0]['option_first'] = TRUE;
				$new_mod['modifier_options'][$modifier_options_count-1]['option_last'] = TRUE;
			}
			else
			{
				$new_mod['modifier_options'] = array(array());
			}

			$new_mod['modifier_select'] = form_dropdown($new_mod['modifier_input_name'], $select_options);
			$new_mod['modifier_input'] = form_input($new_mod['modifier_input_name']);

			$tag_vars[0]['modifiers'][] = $new_mod;
		}

		// sku-related fields really only make sense if there is only one sku per product..
		if (count($product['stock']) == 1)
		{
			$tag_vars[0]['sku'] = $product['stock'][0]['sku'];
			$tag_vars[0]['track_stock'] = $product['stock'][0]['track_stock'] == 'y';
			$tag_vars[0]['stock_level'] = $tag_vars[0]['track_stock'] ? $product['stock'][0]['stock_level'] : FALSE;
		}
		else
		{
			$tag_vars[0]['sku'] = FALSE;
			$tag_vars[0]['track_stock'] = FALSE;
			$tag_vars[0]['stock_level'] = FALSE;
		}

		$hidden_fields = array(
			'entry_id' => $entry_id,
			'return_url' => $this->EE->uri->uri_string,
		);

		if ($this->EE->TMPL->fetch_param('return') !== FALSE)
		{
			$hidden_fields['return_url'] = $this->EE->TMPL->fetch_param('return');
		}
		if ($this->EE->TMPL->fetch_param('empty_cart') == 'yes')
		{
			$hidden_fields['empty_cart'] = 1;
		}

		// are there any custom text input fields?
		$custom_inputs = array();
		foreach ($this->EE->TMPL->tagparams as $param => $name)
		{
			if (strpos($param, 'input:') === 0)
			{
				$custom_inputs[substr($param, 6)] = $name;
			}
		}

		if ( ! empty($custom_inputs))
		{
			$hidden_fields['custom_inputs'] = base64_encode(serialize($custom_inputs));
			$hidden_fields['custom_inputs'] =
				sha1($hidden_fields['custom_inputs'].$this->EE->config->item('license_number')).':'.
				$hidden_fields['custom_inputs'];
		}

		// start our form output
		$out = $this->_form_open(array(
			'action' => $this->EE->store_model->get_action_url('act_add_to_cart'),
			'hidden_fields' => $hidden_fields,
			'class' => 'store_product_form',
		));

		// parse tagdata variables
		$out .= $this->EE->TMPL->parse_variables($this->EE->TMPL->tagdata, $tag_vars);

		// end form output and return
		$out .= '</form>';

		// include product stock javascript
		$out .= '
			<script type="text/javascript">
			window.ExpressoStore = window.ExpressoStore || {};
			ExpressoStore.stock = ExpressoStore.stock || {};
			ExpressoStore.stock['.$entry_id.'] = '.$this->EE->javascript->generate_json($product['stock'], TRUE).';
			'.$this->_async_store_js().'
			</script>';
		return $out;
	}

	protected function _async_store_js()
	{
		$theme_url = str_ireplace('http://', '//', URL_THIRD_THEMES.'store/store.js');

		return '
			(function() { if (!ExpressoStore.scriptElement) {
				var script = ExpressoStore.scriptElement = document.createElement("script");
				script.type = "text/javascript"; script.async = true;
				script.src = "'.$theme_url.'";
				(document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(script);
			}})();';
	}

	public function act_add_to_cart()
	{
		$entry_id = (int)$this->EE->input->get_post('entry_id');

		// do we need to empty the cart first? (useful on single product stores)
		if ($this->EE->input->get_post('empty_cart')) $this->EE->store_cart->empty_cart();

		// add product to cart
		$item_qty = $this->EE->input->get_post('item_qty') === FALSE ? 1 : (int)$this->EE->input->get_post('item_qty');
		$mod_values = $this->EE->input->get_post('modifiers', TRUE);

		// are there any custom input fields?
		$input_values = array();
		if ($custom_inputs = $this->EE->input->get_post('custom_inputs'))
		{
			// verify custom_inputs has not been tampered with
			$custom_inputs = explode(':', $custom_inputs);
			if (count($custom_inputs) == 2 AND
				$custom_inputs[0] == sha1($custom_inputs[1].$this->EE->config->item('license_number')))
			{
				// get submitted values
				$custom_inputs = unserialize(base64_decode($custom_inputs[1]));
				foreach ($custom_inputs as $param => $name)
				{
					// only use param if it was submitted
					if ($this->EE->input->get_post($param) !== FALSE)
					{
						$input_values[$name] = $this->EE->input->get_post($param, TRUE);
					}
				}
			}
			else
			{
				$this->EE->output->set_status_header(400); // bad request
				exit;
			}
		}

		$this->EE->store_cart->insert($entry_id, $item_qty, $mod_values, $input_values);
		$this->EE->session->set_flashdata('store_cart_updated', TRUE);

		// redirect
		$this->EE->functions->redirect($this->_get_return_url());
	}

	public function search()
	{
		$this->EE->load->model('store_products_model');

		$options = array(
			'price_min' => (float)$this->EE->TMPL->fetch_param('search:price:min'),
			'price_max' => (float)$this->EE->TMPL->fetch_param('search:price:max'),
			'on_sale' => $this->EE->TMPL->fetch_param('search:on_sale'),
			'in_stock' => $this->EE->TMPL->fetch_param('search:in_stock'),
		);

		// remove Store-specific params from channel entries tag
		unset($this->EE->TMPL->tagparams['search:price:min']);
		unset($this->EE->TMPL->tagparams['search:price:max']);
		unset($this->EE->TMPL->tagparams['search:on_sale']);
		unset($this->EE->TMPL->tagparams['search:in_stock']);

		$orderby = $this->EE->TMPL->fetch_param('orderby');
		if (in_array($orderby, array('price', 'regular_price', 'total_stock')))
		{
			$options['orderby'] = $orderby;
			$options['sort'] = $this->EE->TMPL->fetch_param('sort');

			// don't pass order & sort params to channel entries tag if we are handling it
			unset($this->EE->TMPL->tagparams['orderby']);
			unset($this->EE->TMPL->tagparams['sort']);

			// entries are already in correct order
			$entries_param = 'fixed_order';
		}
		else
		{
			$entries_param = 'entry_id';
		}

		$entry_ids = $this->EE->store_products_model->find_all_entry_ids($options);

		// only the first parsed tag has access to this variable
		if (empty($entry_ids)) return $this->EE->TMPL->no_results;

		// pass everything else through to channel entries loop
		$this->EE->TMPL->tagparams[$entries_param] = implode('|', $entry_ids);

		if ( ! class_exists('Channel'))
		{
			require_once(APPPATH.'modules/channel/mod.channel.php');
		}

		$channel = new Channel();
		return $channel->entries();
	}

	/**
	 * {exp:store:cart} provides a simple non-interactive method to display the current cart contents,
	 * for use in a sidebar etc
	 */
	public function cart()
	{
		$this->_tmpl_secure_check(FALSE);

		$tagdata = $this->EE->TMPL->tagdata;
		$tag_vars = array($this->EE->store_cart->contents());
		$tag_vars[0]['cart_updated'] = $this->EE->session->flashdata('store_cart_updated');

		// check for empty cart
		if ($this->EE->store_cart->is_empty()) return tmpl_no_results($tagdata, 'no_items');

		return $this->EE->TMPL->parse_variables($tagdata, $tag_vars);
	}

	/**
	 * {exp:store:checkout} is the main tag used for displaying the cart to update totals
	 * and submit orders
	 */
	public function checkout()
	{
		$this->_tmpl_secure_check();

		$this->EE->load->library(array('store_config', 'store_payments'));

		$tagdata = $this->EE->TMPL->tagdata;
		$tag_vars = array($this->EE->store_cart->contents());
		$tag_vars[0]['cart_updated'] = $this->EE->session->flashdata('store_cart_updated');

		$country_list = $this->EE->store_shipping_model->get_countries(TRUE, TRUE);
		$region_list = array();
		foreach ($country_list as $key => $value)
		{
			foreach ($value['regions'] as $region_code => $region_name)
			{
				$region_list[$key][$region_code] = $region_name;
			}
			$country_list[$key] = $value['name'];
		}

		$order_fields = $this->EE->store_config->get_order_fields();
		foreach ($order_fields as $field_name => $field)
		{
			$tag_vars[0]['error:'.$field_name] = FALSE;
		}

		$tag_vars[0]['error:promo_code'] = FALSE;

		// process any POST data for this form
		if ( ! empty($_POST) AND $this->EE->input->post('form_name') == $this->EE->TMPL->fetch_param('form_name'))
		{
			$this->_secure_forms_check();
			$this->_load_validation_library();

			if ($this->EE->input->post('empty_cart'))
			{
				$this->EE->store_cart->empty_cart();
				$this->EE->functions->redirect($this->_get_current_url());
			}

			// update order details and cart quantities
			$update_data = $this->EE->security->xss_clean($_POST);

			if (empty($update_data['items']) OR ! is_array($update_data['items']))
			{
				$update_data['items'] = array();
			}

			// do we need to remove any items?
			if (isset($update_data['remove_items']) AND is_array($update_data['remove_items']))
			{
				foreach ($update_data['remove_items'] as $key => $value)
				{
					if ( ! empty($value)) $update_data['items'][$key]['item_qty'] = 0;
				}
			}

			// remember whether return_url in cart should be https
			if (isset($update_data['return_url']))
			{
				$update_data['return_url'] = $this->_get_return_url();
			}
			$update_data['cancel_url'] = $this->_get_current_url();

			// check whether billing_same_as_shipping and shipping_same_as_billing checkboxes
			// were in the template tagdata but not submitted (i.e. were unchecked by the user)
			// TODO: replace with hidden unchecked fields
			foreach (array('billing_same_as_shipping', 'shipping_same_as_billing') as $field_name)
			{
				// handle both valid name="blah" and name='blah' html attributes
				if ( ! isset($update_data[$field_name]) AND
					(stripos($tagdata, 'name="'.$field_name.'"') OR stripos($tagdata, "name='".$field_name."'")))
				{
					$update_data[$field_name] = FALSE;
				}

				// convert to bool, make sure template matches updated value
				if (isset($update_data[$field_name]))
				{
					$update_data[$field_name] = ( ! empty($update_data[$field_name])) AND $update_data[$field_name] != 'n';
					$tag_vars[0][$field_name] = $update_data[$field_name];
				}
			}

			$require_fields = explode('|', $this->EE->TMPL->fetch_param('require'));

			// if order_email was POSTed, or this is the final checkout step, ensure we have an order_email
			if (isset($update_data['order_email']) OR isset($_POST['submit']))
			{
				$require_fields[] = 'order_email';
			}
			// same for payment_method
			if (isset($update_data['payment_method']) OR isset($_POST['submit']))
			{
				$this->EE->form_validation->set_rules('payment_method', 'lang:payment_method', 'required|valid_payment_method');
			}

			// quick fields are for lazy people when templating to save repeating billing/shipping
			$quick_fields = array('name', 'address1', 'address2', 'address3', 'region', 'country', 'postcode', 'phone');
			foreach ($quick_fields as $field_name)
			{
				if (in_array($field_name, $require_fields))
				{
					$require_fields[] = 'billing_'.$field_name;
					$require_fields[] = 'shipping_'.$field_name;
				}

				if ($rules = $this->EE->TMPL->fetch_param('rules:'.$field_name))
				{
					$this->EE->TMPL->tagparams['rules:billing_'.$field_name] = $rules;
					$this->EE->TMPL->tagparams['rules:shipping_'.$field_name] = $rules;
				}
			}

			foreach ($order_fields as $field_name => $field)
			{
				// load POST data into $tag_vars
				if (isset($update_data[$field_name]))
				{
					// if the form is invalid we will display this data back to user
					$tag_vars[0][$field_name] = $update_data[$field_name];
				}
				else
				{
					// if field wasn't submitted, dump existing value back into POST array
					// so that validation takes into account existing cart data
					$_POST[$field_name] = $tag_vars[0][$field_name];
				}

				// don't show shipping error messages if same as billing and vice versa
				// use updated tag variables here as value may not necessarily have been updated
				if ($tag_vars[0]['shipping_same_as_billing'] AND strpos($field_name, 'shipping_') === 0)
				{
					continue;
				}

				if ($tag_vars[0]['billing_same_as_shipping'] AND strpos($field_name, 'billing_') === 0)
				{
					continue;
				}

				$rules = $this->EE->TMPL->fetch_param('rules:'.$field_name);
				if (in_array($field_name, $require_fields)) $rules .= '|required';

				// ensure order_email field contains the valid_email rule
				if ($field_name == 'order_email') $rules .= '|valid_email';

				if ( ! empty($rules))
				{
					$this->EE->form_validation->set_rules($field_name, 'lang:'.$field_name, $rules);
				}
			}

			// shipping_method is actually stored in cart as shipping_method_id
			if (isset($update_data['shipping_method']))
			{
				$update_data['shipping_method_id'] = $update_data['shipping_method'];
			}

			// shipping method may also be required
			$shipping_method_rules = $this->EE->TMPL->fetch_param('rules:shipping_method');
			if (in_array('shipping_method', $require_fields)) $shipping_method_rules .= '|required';
			if ( ! empty($shipping_method_rules))
			{
				$this->EE->form_validation->set_rules('shipping_method', 'lang:shipping_method', $shipping_method_rules);
			}

			if ( ! empty($update_data['remove_promo_code']))
			{
				$update_data['promo_code'] = '';
			}

			if (isset($update_data['promo_code']))
			{
				$tag_vars[0]['promo_code'] = $update_data['promo_code'];
				$this->EE->form_validation->set_rules('promo_code', 'lang:promo_code', 'valid_promo_code');
			}

			// validate form
			$valid_form = $this->EE->form_validation->run();

			// if this is the final step, shipping method must also validate
			if (isset($_POST['submit']) AND $tag_vars[0]['error:shipping_method'])
			{
				$valid_form = FALSE;
			}

			if ($valid_form)
			{
				// update cart
				$this->EE->store_cart->update($update_data);

				// where to next?
				// we use isset instead of input->post() because some servers seem to treat
				// unicode chars as false
				if (isset($_POST['submit']))
				{
					if ($this->EE->store_config->item('force_member_login') == 'y' AND
						empty($this->EE->session->userdata['member_id']))
					{
						// admin has set order submission to members only,
						// but customer is not logged in
						$this->EE->output->show_user_error(FALSE, array(lang('submit_order_not_logged_in')));
					}

					// create order
					$order = $this->EE->store_cart->submit();

					if ($order['is_order_paid'])
					{
						// skip payment for free orders
						$this->EE->store_payments->redirect_to_return_url($order);
					}

					// submit to payment gateway (this will either redirect to a third party site,
					// or the order's return or cancel url)
					$payment = $this->EE->input->post('payment');
					$this->EE->store_payments->process($order, $payment);
				}
				elseif (isset($_POST['next']) AND isset($_POST['next_url']))
				{
					$this->EE->functions->redirect($this->_get_return_url('next_url'));
				}

				// maybe they just called update totals or something
				$this->EE->functions->redirect($this->_get_current_url());
			}
			else
			{
				$form_errors = $this->EE->form_validation->error_array();

				foreach ($form_errors as $field => $error)
				{
					// use error() method to wrap error message
					$tag_vars[0]['error:'.$field] = $this->EE->form_validation->error($field);
				}

				if ($tag_vars[0]['error:shipping_method'] AND empty($form_errors['shipping_method']))
				{
					$form_errors['shipping_method'] = $tag_vars[0]['error:shipping_method'];
				}

				if ($this->EE->TMPL->fetch_param('error_handling') != 'inline')
				{
					$this->EE->output->show_user_error(FALSE, $form_errors);
				}

				// ensure developer has actually templated the inline error messages
				foreach ($form_errors as $field => $error)
				{
					if (strpos($tagdata, "error:$field") === FALSE)
					{
						$this->EE->output->show_user_error(FALSE, $form_errors);
					}
				}
			}
		}

		// wrap our non-CI form validation lib errors
		$tag_vars[0]['error:shipping_method'] = $this->_wrap_error($tag_vars[0]['error:shipping_method']);

		// check for empty cart
		if ($this->EE->store_cart->is_empty()) return tmpl_no_results($tagdata, 'no_items');

		// load available shipping & payment methods
		$tag_vars[0]['shipping_methods'] = array();
		$tag_vars[0]['shipping_method_options'] = '';

		$shipping_methods = $this->EE->store_shipping_model->get_all_shipping_methods(TRUE);
		foreach ($shipping_methods as $row)
		{
			$method = array(
				'method_id' => $row['shipping_method_id'],
				'method_title' => $row['title'],
				'method_selected' => $tag_vars[0]['shipping_method_id'] == $row['shipping_method_id'] ? ' selected="selected" ' : '',
			);
			$tag_vars[0]['shipping_method_options'] .= '<option value="'.$method['method_id'].'"'.$method['method_selected'].'>'.$method['method_title'].'</option>';
			$tag_vars[0]['shipping_methods'][] = $method;
		}

		// load available countries and regions
		$enabled_countries = $this->EE->store_shipping_model->get_countries(TRUE, TRUE);
		$tag_vars[0]['billing_country_options'] = $this->_get_country_options($enabled_countries, $tag_vars[0]['billing_country']);
		$tag_vars[0]['shipping_country_options'] = $this->_get_country_options($enabled_countries, $tag_vars[0]['shipping_country']);
		$tag_vars[0]['billing_region_options'] = $this->_get_region_options($enabled_countries, $tag_vars[0]['billing_country'], $tag_vars[0]['billing_region']);
		$tag_vars[0]['shipping_region_options'] = $this->_get_region_options($enabled_countries, $tag_vars[0]['shipping_country'], $tag_vars[0]['shipping_region']);

		// helper variables for checkboxes
		$tag_vars[0]['shipping_same_as_billing_checked'] = $tag_vars[0]['shipping_same_as_billing'] ? 'checked="checked"' : NULL;
		$tag_vars[0]['billing_same_as_shipping_checked'] = $tag_vars[0]['billing_same_as_shipping'] ? 'checked="checked"' : NULL;

		// form input helpers
		foreach ($order_fields as $field_name => $field)
		{
			$field_type = $field_name == 'order_email' ? 'email' : 'text';
			$tag_vars[0]['field:'.$field_name] = '<input type="'.$field_type.'" '.
				'id="'.$field_name.'" name="'.$field_name.'" value="'.$tag_vars[0][$field_name].'" />';
		}
		foreach (array('billing_region', 'billing_country', 'shipping_region', 'shipping_country', 'shipping_method') as $field_name)
		{
			$tag_vars[0]['field:'.$field_name] = '<select id="'.$field_name.'" name="'.$field_name.'">'.$tag_vars[0][$field_name.'_options'].'</select>';
		}
		foreach (array('shipping_same_as_billing', 'billing_same_as_shipping') as $field_name)
		{
			$tag_vars[0]['field:'.$field_name] = '<input type="hidden" name="'.$field_name.'" value="0" />'.
				'<input type="checkbox" id="'.$field_name.'" name="'.$field_name.'" value="1" '.$tag_vars[0][$field_name.'_checked'].' />';
		}
		$tag_vars[0]['field:promo_code'] = '<input type="text" id="promo_code" name="promo_code" value="'.$tag_vars[0]['promo_code'].'" />';

		// store regions array as js array
		$out = '<script type="text/javascript">
			window.ExpressoStore = window.ExpressoStore || {};
			ExpressoStore.countries = '.$this->EE->javascript->generate_json($enabled_countries, TRUE).';
			'.$this->_async_store_js().'
		</script>';

		$hidden_fields = array(
			'form_name' => $this->EE->TMPL->fetch_param('form_name'),
			'return_url' => $this->EE->uri->uri_string,
		);

		$this->_add_payment_tag_vars($tag_vars);
		if (($payment_method = $this->EE->TMPL->fetch_param('payment_method')) != FALSE)
		{
			$hidden_fields['payment_method'] = $payment_method;
		}

		// previous_url variable helpful for a "continue shopping" link
		$tag_vars[0]['previous_url'] = isset($this->EE->session->tracker[1]) ?
			$this->EE->functions->create_url($this->EE->session->tracker[1]) : FALSE;

		foreach (array('next', 'return') as $param)
		{
			if ($this->EE->TMPL->fetch_param($param))
			{
				$hidden_fields[$param.'_url'] = $this->EE->TMPL->fetch_param($param);
			}
		}

		// start our form output
		$out .= $this->_form_open(array(
			'action' => $this->_get_current_url(),
			'hidden_fields' => $hidden_fields,
			'data-order-total' => $tag_vars[0]['order_total_val'],
		));

		// parse tagdata variables
		$out .= $this->EE->TMPL->parse_variables($tagdata, $tag_vars);

		// end form output and return
		$out .= '</form>';
		return $out;
	}

	/**
	 * Standard form opening tag
	 */
	protected function _form_open($data)
	{
		$defaults = array(
			'action' => $this->EE->functions->fetch_site_index(),
			'method' => 'post',
			'id' => $this->EE->TMPL->fetch_param('form_id'),
			'name' => $this->EE->TMPL->fetch_param('form_name'),
			'class' => '',
			'enctype' => '',
		);

		$data = array_merge($defaults, $data);

		// class gets appended
		$data['class'] = trim($data['class'].' '.$this->EE->TMPL->fetch_param('form_class'));

		if (empty($data['hidden_fields']))
		{
			$hidden_fields = array();
		}
		else
		{
			$hidden_fields = $data['hidden_fields'];
			unset($data['hidden_fields']);
		}

		$hidden_fields['site_id'] = $this->EE->config->item('site_id');

		if ($this->EE->config->item('secure_forms') == 'y')
		{
			$hidden_fields['XID'] = '{XID_HASH}';
		}

		if ($this->EE->TMPL->fetch_param('secure_action') == 'yes')
		{
			$data['action'] = str_replace('http://', 'https://', $data['action']);
		}

		if ($this->EE->TMPL->fetch_param('secure_return') == 'yes')
		{
			$hidden_fields['secure_return'] = 1;
		}

		// Add the CSRF Protection Hash
		if ($this->EE->config->item('csrf_protection') == TRUE)
		{
			$hidden_fields[$this->EE->security->get_csrf_token_name()] = $this->EE->security->get_csrf_hash();
		}

		if ($data['enctype'] == 'multi' OR strtolower($data['enctype']) == 'multipart/form-data')
		{
			$data['enctype'] = 'multipart/form-data';
		}

		$out = '<form ';

		foreach ($data as $key => $value)
		{
			if ($value !== '')
			{
				$out .= htmlspecialchars($key).'="'.htmlspecialchars($value).'" ';
			}
		}

		$out .= ">\n<div style=\"margin:0;padding:0;display:inline\">\n";

		foreach ($hidden_fields as $key => $value)
		{
			$out .= '<input type="hidden" name="'.htmlspecialchars($key).'" value="'.htmlspecialchars($value).'" />'."\n";
		}

		$out .= "</div>\n\n";
		return $out;
	}

	/**
	 * Generates a list of HTML <option> tags for the list of countries
	 */
	private function _get_country_options($enabled_countries, $selected_country)
	{
		$options = array();

		foreach ($enabled_countries as $country_code => $country)
		{
			if ($country_code == $selected_country)
			{
				$options[] = '<option value="'.$country_code.'" selected="selected">'.$country['name'].'</option>';
			}
			else
			{
				$options[] = '<option value="'.$country_code.'">'.$country['name'].'</option>';
			}
		}

		if (empty($options))
		{
			return '<option></option>';
		}
		else
		{
			return implode("\n", $options);
		}
	}

	/**
	 * Generates a list of HTML <option> tags for the list of regions
	 */
	private function _get_region_options($enabled_countries, $selected_country, $selected_region)
	{
		$options = array();

		// find and display the appropriate list of regions
		if (isset($enabled_countries[$selected_country]))
		{
			foreach ($enabled_countries[$selected_country]['regions'] as $region_code => $region_name)
			{
				if ($region_code == $selected_region)
				{
					$options[] = '<option value="'.$region_code.'" selected="selected">'.$region_name.'</option>';
				}
				else
				{
					$options[] = '<option value="'.$region_code.'">'.$region_name.'</option>';
				}
			}
		}

		if (empty($options))
		{
			return '<option></option>';
		}
		else
		{
			return implode("\n", $options);
		}
	}

	/**
	 * Load the form validation library and configure it
	 * with error delimiters specified in the template.
	 */
	protected function _load_validation_library()
	{
		$this->EE->load->library('store_form_validation');

		$error_delimiters = explode('|', $this->EE->TMPL->fetch_param('error_delimiters'));
		if (count($error_delimiters) == 2)
		{
			$this->EE->form_validation->set_error_delimiters($error_delimiters[0], $error_delimiters[1]);
		}
	}

	/**
	 * Wrap an error message with delimiters specified in the template
	 */
	protected function _wrap_error($message)
	{
		if (empty($message)) return FALSE;

		$error_delimiters = explode('|', $this->EE->TMPL->fetch_param('error_delimiters'));
		if (count($error_delimiters) == 2)
		{
			return $error_delimiters[0].$message.$error_delimiters[1];
		}

		return $message;
	}

	private function _tmpl_secure_check($use_global = TRUE)
	{
		if ($this->EE->TMPL->fetch_param('secure') == 'yes' OR
			($use_global AND $this->EE->store_config->item('secure_template_tags') == 'y'))
		{
			if ($this->EE->store_model->secure_request())
			{
				// connection is secure - good. make sure form submissions are secure too
				$this->EE->TMPL->tagparams['secure_action'] = 'yes';
				$this->EE->TMPL->tagparams['secure_return'] = 'yes';
			}
			else
			{
				$this->EE->functions->redirect(str_replace('http://', 'https://', $this->EE->functions->fetch_current_uri()));
			}
		}
	}

	protected function _get_current_url()
	{
		$url = $this->EE->functions->fetch_current_uri();
		if ($this->EE->store_model->secure_request())
		{
			$url = str_replace('http://', 'https://', $url);
		}
		return $url;
	}

	protected function _get_return_url($name = 'return_url')
	{
		$url = $this->EE->functions->create_url($this->EE->input->post($name));
		if ($this->EE->input->post('secure_return'))
		{
			$url = str_replace('http://', 'https://', $url);
		}
		return $url;
	}

	protected function _add_payment_tag_vars(&$tag_vars)
	{
		// these fields are deprecated and will be removed in a future version
		$tag_vars[0]['payment_status'] = FALSE;
		$tag_vars[0]['payment_message'] = FALSE;
		$tag_vars[0]['payment_txn_id'] = FALSE;

		// ensure we have an error:payment_method
		if ( ! isset($tag_vars[0]['error:payment_method']))
		{
			$tag_vars[0]['error:payment_method'] = FALSE;
		}

		if (($payment_error = $this->EE->session->flashdata('store_payment_error')) !== FALSE)
		{
			$tag_vars[0]['error:payment_method'] = $this->_wrap_error($payment_error);

			// support deprecated payment_status variable
			$tag_vars[0]['payment_status'] = 'failed';
			$tag_vars[0]['payment_message'] = $payment_error;
		}

		// payment_method_options variable for lazy people
		$tag_vars[0]['payment_method_options'] = '';
		if (strpos($this->EE->TMPL->tagdata, '{payment_method_options}') !== FALSE)
		{
			if (($payment_method = $this->EE->TMPL->fetch_param('payment_method')) == FALSE)
			{
				$payment_method = $tag_vars[0]['payment_method'];
			}

			$tag_vars[0]['payment_method_options'] = $this->EE->store_payments_model->enabled_payment_methods_select($payment_method);
		}

		$tag_vars[0]['field:payment_method'] = '<select id="payment_method" name="payment_method">'.$tag_vars[0]['payment_method_options'].'</select>';

		$tag_vars[0]['exp_month_options'] = $this->EE->store_payments->get_exp_month_options();
		$tag_vars[0]['exp_year_options'] = $this->EE->store_payments->get_exp_year_options();
	}

	/**
	 * Ensure a submitted form has a valid XID token
	 */
	protected function _secure_forms_check()
	{
		if ( ! $this->EE->security->secure_forms_check($this->EE->input->post('XID')))
		{
			$this->EE->functions->redirect($this->_get_current_url());
		}
	}

	/*
	 * Parameters - order_id, order_hash, member_id, orderby, sort, limit
	 * order_id overrides anything else order_id pipe seperated but only 0-1 order_hash
	 * member_id can be pipe seperated as well
	 *
	 * start_on? stop_before? status
	 * orderby and sort pipe seperated?
	 *
	 */
	public function orders()
	{
		$this->_tmpl_secure_check();

		$this->EE->load->model('store_orders_model');

		$query = array(
			'order_id' => $this->EE->TMPL->fetch_param('order_id'),
			'order_hash' => $this->EE->TMPL->fetch_param('order_hash'),
			'member_id' => $this->EE->TMPL->fetch_param('member_id'),
			'orderby' => $this->EE->TMPL->fetch_param('orderby'),
			'sort' => $this->EE->TMPL->fetch_param('sort'),
			'limit' => (int)$this->EE->TMPL->fetch_param('limit'),
			'offset' => (int)$this->EE->TMPL->fetch_param('offset'),
		);

		if (($paid = $this->EE->TMPL->fetch_param('paid')) != '')
		{
			$query['is_order_paid'] = $paid == 'yes';
		}

		$tag_vars = $this->EE->store_orders_model->get_orders_tag($query);

		if (empty($tag_vars)) return tmpl_no_results($this->EE->TMPL->tagdata, 'no_orders');

		$out = $this->EE->TMPL->parse_variables($this->EE->TMPL->tagdata, $tag_vars);

		return $out.$this->_do_analytics($tag_vars);
	}

	public function payment()
	{
		$this->_tmpl_secure_check();

		$this->EE->load->model(array('store_orders_model', 'store_shipping_model'));
		$this->EE->load->library('store_payments');

		$query = array(
			'order_id' => $this->EE->TMPL->fetch_param('order_id'),
			'order_hash' => $this->EE->TMPL->fetch_param('order_hash'),
			'member_id' => $this->EE->TMPL->fetch_param('member_id'),
			'limit' => 1,
		);

		// either order_id or order_hash must be specified
		if ($query['order_id'] === FALSE)
		{
			$query['order_hash'] = (string)$query['order_hash'];
		}
		else
		{
			$query['order_id'] = (int)$query['order_id'];
		}

		$matching_orders = $this->EE->store_orders_model->get_orders_tag($query);

		if (empty($matching_orders))
		{
			return tmpl_no_results($this->EE->TMPL->tagdata, 'no_orders');
		}

		$order = $matching_orders[0];

		if ( ! empty($_POST))
		{
			$this->_secure_forms_check();
			$this->_load_validation_library();

			$order['payment_method'] = $this->EE->input->post('payment_method', TRUE);
			$order['return_url'] = $this->_get_return_url();
			$order['cancel_url'] = $this->_get_current_url();
			$this->EE->store_payments->return_if_already_paid($order);

			$this->EE->load->library('store_form_validation');
			$this->EE->form_validation->set_rules('payment_method', 'lang:payment_method', 'required|valid_payment_method');

			if ($this->EE->form_validation->run())
			{
				// save new payment method and return url
				$this->EE->store_orders_model->update_order($order['order_id'], array(
					'payment_method' => $order['payment_method'],
					'return_url' => $order['return_url'],
					'cancel_url' => $order['cancel_url'],
				));

				// process payment info
				$payment = $this->EE->input->post('payment');
				$this->EE->store_payments->process($order, $payment);
			}
			else
			{
				$order['error:payment_method'] = $this->EE->form_validation->error('payment_method');
			}
		}

		$tag_vars = array($order);

		$hidden_fields = array(
			'return_url' => $this->EE->uri->uri_string
		);

		$this->_add_payment_tag_vars($tag_vars);
		if (($payment_method = $this->EE->TMPL->fetch_param('payment_method')) != FALSE)
		{
			$hidden_fields['payment_method'] = $payment_method;
		}

		// start our form output
		$out = $this->_form_open(array(
			'action' => $this->_get_current_url(),
			'hidden_fields' => $hidden_fields,
			'data-order-total' => $tag_vars[0]['order_total_val'],
		));

		// parse tagdata variables
		$out .= $this->EE->TMPL->parse_variables($this->EE->TMPL->tagdata, $tag_vars);

		// end form output and return
		$out .= '</form>';
		return $out.$this->_do_analytics($tag_vars);
	}

	/**
	 * Insert Google Analytics tracking data if order has recently been completed
	 * We use cookies to ensure this only happens once per order
	 */
	protected function _do_analytics($tag_vars)
	{
		$order_hash = $this->EE->input->cookie('cmcartsubmit');
		if (empty($order_hash)) return;

		// does the current page actually contain the submitted order?
		$order = FALSE;
		foreach ($tag_vars as $tag_var)
		{
			if ($tag_var['order_hash'] == $order_hash)
			{
				// user has just completed the order, this must be an "order completed" page
				$order = $tag_var;
			}
		}

		if (empty($order)) return;

		$ga = array();
		$ga[] = array(
			'_addTrans',
			$order['order_id'],
			$this->EE->config->item('site_name'),
			$order['order_total_val'],
			$order['order_tax_val'],
			$order['order_shipping_val'],
			$order['billing_address3'],
			$order['billing_region_name'],
			$order['billing_country_name']);

		// GA will only allow one entry per sku
		// we need to aggregate any items in cart which have the same sku
		$items = array();
		foreach ($order['items'] as $item)
		{
			$sku = $item['sku'];
			if (isset($items[$sku]))
			{
				// sku exists, just increase quantity
				$items[$sku]['item_qty'] += $item['item_qty'];
			}
			else
			{
				$items[$sku] = $item;
			}
		}

		foreach ($items as $item)
		{
			$ga[] = array(
				'_addItem',
				$order['order_id'],
				$item['title'],
				'',
				$item['price'],
				$item['item_qty'],
			);
		}

		$ga[] = array('_trackTrans');

		$out = "\n<script type='text/javascript'>\nvar _gaq = _gaq || [];";

		foreach ($ga as $command)
		{
			$command = $this->EE->javascript->generate_json($command, TRUE);
			$out .= "\n_gaq.push($command);";
		}

		$out .= "\n</script>";

		// GA tracking should only ever happen once per order, unset cookie
		$this->EE->functions->set_cookie('cmcartsubmit');

		return $out;
	}

	public function download()
	{
		$this->EE->load->model('store_orders_model');
		$url = $this->EE->TMPL->fetch_param('url');
		if (empty($url)) return;

		$order_id = (int)$this->EE->TMPL->fetch_param('order_id');
		$expire = (int)$this->EE->TMPL->fetch_param('expire');

		$order = $this->EE->store_orders_model->find_by_id($order_id);
		if (empty($order) OR $order['is_order_unpaid']) return;

		// make sure download hasn't expired
		if ($this->_is_download_expired($order, $expire)) return;

		$file_id = (int)$this->EE->store_model->get_file_id(dirname($url), basename($url));

		if ($file_id <= 0) return '<span style="font-weight: bold; color: red;">'.lang('download_not_found').'</span>';

		$params = array('o' => $order_id, 'f' => $file_id);
		if ($expire > 0) $params['e'] = $expire;

		// generate file download key to verify parameters (prevents people guessing download URLs)
		$params['k'] = $this->_generate_download_key($order, $file_id, $expire);

		$out = '<a href="'.$this->EE->store_model->get_action_url('act_download_file').AMP.
			http_build_query($params).'"';

		foreach (array('id', 'class', 'style') as $param)
		{
			if (($param_val = $this->EE->TMPL->fetch_param($param)) !== FALSE)
			{
				$out .= ' '.$param.'="'.$param_val.'"';
			}
		}
		$out .= '">'.$this->EE->TMPL->tagdata.'</a>';
		return $out;
	}

	public function act_download_file()
	{
		$this->EE->load->model('store_orders_model');

		$order_id = (int)$this->EE->input->get('o');
		$file_id = (int)$this->EE->input->get('f');
		$expire = (int)$this->EE->input->get('e');
		$key = $this->EE->input->get('k');

		$order = $this->EE->store_orders_model->find_by_id($order_id);
		if (empty($order) OR $order['is_order_unpaid']) show_404();

		// make sure download key matches
		if ($key !== $this->_generate_download_key($order, $file_id, $expire)) show_404();

		// make sure download link hasn't expired
		if ($this->_is_download_expired($order, $expire))
		{
			exit(lang('download_link_expired'));
		}

		$filepath = $this->EE->store_model->get_file($file_id);
		if (empty($filepath)) show_404();
		if ( ! file_exists($filepath)) show_404();

		$path_parts = pathinfo($filepath);
		$extension = $path_parts['extension'];
		$filename = $path_parts['basename'];

		// Load the mime types
		@include(APPPATH.'config/mimes.php');

		// Set a default mime if we can't find it
		$mime = isset($mimes[$extension]) ? $mimes[$extension] : 'application/octet-stream';
		if (is_array($mime)) $mime = $mime[0];

		// dump the file data
		header('Content-Type: "'.$mime.'"');
		header('Content-Disposition: attachment; filename="'.$filename.'"');

		/* Hidden config option: store_download_output_method
		 * If set to 'xsendfile', downloads will be sent with the X-Sendfile header.
		 * This gives better performance, but has not been thoroughly tested yet.
		 */
		if ($this->EE->config->item('store_download_output_method') === 'xsendfile')
		{
			header('X-Sendfile: '.$filepath);
			exit;
		}

		header('Content-Transfer-Encoding: binary');
		header('Expires: 0');
		header('Content-Length: '.filesize($filepath));

		if (strpos($_SERVER['HTTP_USER_AGENT'], "MSIE") !== FALSE)
		{
			header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
			header('Pragma: public');
		}
		else
		{
			header('Pragma: no-cache');
		}

		readfile($filepath);
		exit;
	}

	/**
	 * Generate secure download key
	 *
	 * Secure enough that someone would need to know your EE license number before
	 * they would be able to brute force a valid download link
	 */
	private function _generate_download_key($order, $file_id, $expire)
	{
		return sha1($order['order_id'].$order['order_hash'].$file_id.$expire.$this->EE->config->item('license_number'));
	}

	private function _is_download_expired($order, $expire)
	{
		return $expire > 0 AND ($order['order_paid_date'] + ($expire * 60) > $this->EE->localize->now);
	}

	public function act_field_stock()
	{
		$this->EE->load->model('store_products_model');

		// get post data for our store product details field
		$post_data = $this->EE->input->post('store_product_field', TRUE);
		if (empty($post_data)) exit('Store');

		$output = $this->EE->store_products_model->generate_stock_matrix_html($post_data, 'store_product_field');
		exit($output);
	}

	public function act_payment_return()
	{
		$this->EE->load->model(array('store_orders_model', 'store_payments_model'));
		$this->EE->load->library('store_payments');

		$payment_hash = (string)$this->EE->input->get('H');

		// find payment
		$payment = $this->EE->store_orders_model->get_payment_by_hash($payment_hash);
		if (empty($payment)) show_error(lang('error_processing_order'));

		// find order
		$order = $this->EE->store_orders_model->find_by_id($payment['order_id']);
		if (empty($order)) show_error(lang('error_processing_order'));

		// process payment
		$this->EE->store_payments->process_return($order, $payment);
	}
}
/* End of file mod.store.php */