diff --git a/README.md b/README.md index 9ae7c604..50f11ed5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Hero Framework ==== -[Hero](http://www.heroframework.com) is a powerful Content Management System (CMS) and eCommerce framework built on CodeIgniter. +Hero is a powerful Content Management System (CMS) and eCommerce framework built on CodeIgniter. ## Documentation @@ -27,11 +27,11 @@ If you experience issues, please review the full Server Requirements article in ## Documentation -* There is a full [user guide with designer, developer, and user documentation](http://www.heroframework.com/user_guide) +* There is a full [user guide with designer, developer, and user documentation](https://web.archive.org/web/20140920204221/http://www.heroframework.com/user_guide/index.html) ## eCommerce -[Hero eCommerce](http://www.github.com/electricfunction/hero-ecommerce) is a free, open-source collection of modules that drop right into your +Hero eCommerce is a free, open-source collection of modules that drop right into your Hero installation and provide you with online store, subscription billing, paywall, and coupon functionality. ## History diff --git a/app/config/default_routes.php b/app/config/default_routes.php index 7a0d0aae..455280e8 100644 --- a/app/config/default_routes.php +++ b/app/config/default_routes.php @@ -32,4 +32,7 @@ $route['checkout'] = 'billing/checkout'; $route['checkout/([a-zA-Z_-]+)'] = 'billing/checkout/$1'; $route['subscriptions'] = 'billing/subscriptions'; -$route['subscriptions/(:any)'] = 'billing/subscriptions/$1'; \ No newline at end of file +$route['subscriptions/(:any)'] = 'billing/subscriptions/$1'; + +// 404 route +$route['404_override'] = 'error/view'; \ No newline at end of file diff --git a/app/controllers/admincp/dashboard.php b/app/controllers/admincp/dashboard.php index 16be31fb..923b6ba2 100644 --- a/app/controllers/admincp/dashboard.php +++ b/app/controllers/admincp/dashboard.php @@ -192,6 +192,20 @@ function activity () { } } + if (module_installed('twitter')) { + // ... published to twitter + $result = $this->db->select('tweet') + ->select('sent_time') + ->from('tweets_sent') + ->order_by('sent_time','DESC') + ->limit(10) + ->get(); + + foreach ($result->result_array() as $tweets) { + $activity[strtotime($tweets['sent_time'])] = $tweets['tweet'] . ' was tweeted on Twitter'; + } + } + // ... published content $result = $this->db->select('content.content_id') ->select('user_first_name') diff --git a/app/libraries/smarty/debug.tpl b/app/libraries/smarty/debug.tpl new file mode 100644 index 00000000..ed2f5e23 --- /dev/null +++ b/app/libraries/smarty/debug.tpl @@ -0,0 +1,137 @@ +{capture name='_smarty_debug' assign=debug_output} + + +
+| ${$vars@key|escape:'html'} | +{$vars|debug_print_var nofilter} | +
|---|
| {$vars@key|escape:'html'} | +{$vars|debug_print_var nofilter} | +
|---|
You must be logged in at this point of checkout. Please login and continue checking out.
'); + + return redirect('checkout/account'); + } + + return TRUE; + } + + /** + * Account + */ + function index () { + $this->get_errors_and_notices(); + $this->prep_cart(); + + // unset coupon + $this->session->set_userdata('active_coupon',FALSE); + + // let's reset everything in our cart + $this->cart_model->reset_to_precoupon(); + + if ($this->user_model->logged_in() === FALSE) { + if ($this->input->get('email') and $this->input->get('email') != '') { + $email = query_value_decode($this->input->get('email')); + } + else { + $email = ''; + } + + $this->smarty->assign('email', $email); + return $this->smarty->display('checkout_templates/account.thtml'); + } + else { + // they are already logged in, pass them forward + redirect('checkout/billing_shipping'); + } + } + + /** + * Account Redirect + */ + function account () { + return $this->index(); + } + + /** + * Post Account + */ + function post_account () { + $this->load->library('form_validation'); + $this->load->helper('form'); + + $this->form_validation->set_rules('type','Account Type','required'); + $this->form_validation->set_rules('email','Email Address','required'); + + if ($this->form_validation->run() === FALSE) { + $this->session->set_userdata('errors',validation_errors()); + + return redirect('checkout?email=' . query_value_encode($this->input->post('email'))); + } + + // are we logging in or creating a new account? + if ($this->input->post('type') == 'existing_account') { + // validate fields + $this->form_validation->set_rules('password','Password','trim|required'); + + if ($this->form_validation->run() == FALSE) { + $this->session->set_userdata('errors',validation_errors()); + + return redirect('checkout?email=' . query_value_encode($this->input->post('email'))); + } + + // attempt login + if ($this->user_model->login($this->input->post('email'), $this->input->post('password'))) { + // success! + // redirect to billing/shipping address step + return redirect('checkout/billing_shipping'); + } + else { + if ($this->user_model->failed_due_to_activation == TRUE) { + $this->session->set_userdata('errors','Login failed. Your account email has not been activated yet. Please click the link in your activation email to activate your account. If you cannot find the email in your inbox or junk folders, contact website support for assistance.'); + } + else { + $this->session->set_userdata('errors','
Login failed. Please verify your email and password.'); + } + + return redirect('checkout?email=' . query_value_encode($this->input->post('email'))); + } + } + else { + // let's make SURE that this email doesn't already have an account + if ($this->user_model->unique_email($this->input->post('email')) === FALSE) { + // this email is already in the system + // let's show them that error and kick them back + $this->session->set_userdata('errors','The email address, "' . $this->input->post('email') . '", is already linked to an account. If you have an account already, please enter your password below and login. You do not need to register again.'); + + return redirect('checkout?email=' . query_value_encode($this->input->post('email'))); + } + else { + return redirect('checkout/register?email=' . query_value_encode($this->input->post('email'))); + } + } + } + + /** + * Register + */ + function register () { + $this->get_errors_and_notices(); + $this->prep_cart(); + + // do we have a return URL? + $return = site_url('checkout/billing_shipping?new_account=true'); + + // get custom fields + $this->load->model('custom_fields_model'); + $custom_fields = $this->custom_fields_model->get_custom_fields(array('group' => '1')); + + // get email + $email = ($this->input->get('email')) ? query_value_decode($this->input->get('email')) : ''; + + $this->smarty->assign('email', $email); + $this->smarty->assign('custom_fields',$custom_fields); + $this->smarty->assign('return', $return); + return $this->smarty->display('checkout_templates/register.thtml'); + } + + /** + * Billing & Shipping Addresses + */ + function billing_shipping () { + // is this a free cart? if so, it doesn't require an address + // by doing this prior to the methods below, we can retain any notices that may need to be shown + $this->load->model('store/cart_model'); + if ($this->cart_model->free_cart()) { + return redirect('checkout/free_confirm'); + } + + $this->get_errors_and_notices(); + $this->prep_cart(); + $this->require_login(); + + // show a new account thank you if we just registered a new account + if ($this->input->get('new_account') == 'true') { + $this->session->set_userdata('notices','
You have successfully created a new account. You may continue your checkout below.
'); + + return redirect('checkout/billing_shipping'); + } + + // do we have a valid billing address on file? + $valid_billing_address = ($this->user_model->validate_billing_address($this->user_model->get('id')) == TRUE) ? TRUE : FALSE; + $billing_address = $this->user_model->get_billing_address($this->user_model->get('id')); + + $this->load->helper('format_street_address'); + $formatted_billing_address = format_street_address($billing_address); + + // get states & countries + $this->load->model('states_model'); + $countries = $this->states_model->GetCountries(); + $states = $this->states_model->GetStates(); + + // billing address values + if ($this->input->get('billing_values')) { + $billing_values = unserialize(query_value_decode($this->input->get('billing_values'))); + } + else { + if (empty($billing_address)) { + $billing_values = array( + 'first_name' => $this->user_model->get('first_name'), + 'last_name' => $this->user_model->get('last_name'), + 'address_1' => '', + 'address_2' => '', + 'company' => '', + 'city' => '', + 'state' => '', + 'country' => '', + 'postal_code' => '', + 'phone_number' => '' + ); + } + else { + $billing_values = array( + 'first_name' => $billing_address['first_name'], + 'last_name' => $billing_address['last_name'], + 'address_1' => $billing_address['address_1'], + 'address_2' => $billing_address['address_2'], + 'company' => $billing_address['company'], + 'city' => $billing_address['city'], + 'state' => $billing_address['state'], + 'country' => $billing_address['country'], + 'postal_code' => $billing_address['postal_code'], + 'phone_number' => $billing_address['phone_number'] + ); + } + } + + // shipping address values + if ($this->input->get('shipping_values')) { + $shipping_values = unserialize(query_value_decode($this->input->get('shipping_values'))); + } + else { + $shipping_values = array( + 'first_name' => $this->user_model->get('first_name'), + 'last_name' => $this->user_model->get('last_name'), + 'company' => '', + 'city' => '', + 'state' => '', + 'country' => '', + 'postal_code' => '', + 'phone_number' => '' + ); + } + + $this->smarty->assign('billing_values', $billing_values); + $this->smarty->assign('shipping_values', $shipping_values); + $this->smarty->assign('countries', $countries); + $this->smarty->assign('states', $states); + $this->smarty->assign('formatted_billing_address',$formatted_billing_address); + $this->smarty->assign('valid_billing_address',$valid_billing_address); + $this->smarty->assign('billing_address', $billing_address); + return $this->smarty->display('checkout_templates/billing_shipping.thtml'); + } + + /** + * Post Billing/Shipping + */ + function post_billing_shipping () { + $this->prep_cart(); + $this->require_login(); + + $this->load->library('form_validation'); + + // new or existing billing address? + if ($this->input->post('billing_address') == 'existing') { + $billing_address_type = 'existing'; + } + else { + $billing_address_type = 'new'; + } + + // billing address validation + + if ($billing_address_type == 'new') { + $this->form_validation->set_rules('first_name','First Name','required'); + $this->form_validation->set_rules('last_name','Last Name','required'); + $this->form_validation->set_rules('address_1','Address','required'); + $this->form_validation->set_rules('city','City','required'); + $this->form_validation->set_rules('country','Country','required'); + if ($this->input->post('state') == '') { + $this->form_validation->set_rules('state_select','State/Province','required'); + } + $this->form_validation->set_rules('postal_code','Postal/Zip Code','required'); + + if (isset($_POST['phone_number'])) { + // we only require this field if it was sent + // it was added in 3.73 and we don't want to break old sites + $this->form_validation->set_rules('phone_number','Phone Number','required'); + } + } + + // shipping address validation + if ($this->requires_shipping == TRUE) { + $this->form_validation->set_rules('shipping_address','Shipping Address Type','required'); + + if ($this->input->post('shipping_address') == 'new') { + $this->form_validation->set_rules('shipping_first_name','First Name','required'); + $this->form_validation->set_rules('shipping_last_name','Last Name','required'); + $this->form_validation->set_rules('shipping_address_1','Address','required'); + $this->form_validation->set_rules('shipping_city','City','required'); + $this->form_validation->set_rules('shipping_country','Country','required'); + if ($this->input->post('shipping_state') == '') { + $this->form_validation->set_rules('shipping_state_select','State/Province','required'); + } + $this->form_validation->set_rules('shipping_postal_code','Postal/Zip Code','required'); + + if (isset($_POST['shipping_phone_number'])) { + // we only require this field if it was sent + // it was added in 3.73 and we don't want to break old sites + $this->form_validation->set_rules('shipping_phone_number','Phone Number','required'); + } + } + } + + // build arrays of values in case we need to redirect back to the form + if ($billing_address_type == 'new') { + $billing_values = array( + 'first_name' => $this->input->post('first_name'), + 'last_name' => $this->input->post('last_name'), + 'company' => $this->input->post('company'), + 'address_1' => $this->input->post('address_1'), + 'address_2' => $this->input->post('address_2'), + 'city' => $this->input->post('city'), + 'country' => $this->input->post('country'), + 'postal_code' => $this->input->post('postal_code'), + 'state' => ($this->input->post('state_select') == '') ? $this->input->post('state') : $this->input->post('state_select'), + 'phone_number' => $this->input->post('phone_number') + ); + } + else { + $billing_values = array(); + } + + if ($this->requires_shipping == TRUE and $this->input->post('shipping_address') == 'new') { + $shipping_values = array( + 'first_name' => $this->input->post('shipping_first_name'), + 'last_name' => $this->input->post('shipping_last_name'), + 'company' => $this->input->post('shipping_company'), + 'address_1' => $this->input->post('shipping_address_1'), + 'address_2' => $this->input->post('shipping_address_2'), + 'city' => $this->input->post('shipping_city'), + 'country' => $this->input->post('shipping_country'), + 'postal_code' => $this->input->post('shipping_postal_code'), + 'state' => ($this->input->post('shipping_state_select') == '') ? $this->input->post('shipping_state') : $this->input->post('shipping_state_select'), + 'phone_number' => $this->input->post('shipping_phone_number') + ); + } + else { + $shipping_values = array(); + } + + if (!empty($this->form_validation->_config_rules) and $this->form_validation->run() === FALSE) { + $this->session->set_userdata('errors',validation_errors()); + + redirect('checkout/billing_shipping?billing_values=' . query_value_encode(serialize($billing_values)) . '&shipping_values=' . query_value_encode(serialize($shipping_values))); + } + + // we are validated + + if ($billing_address_type == 'new') { + // update their billing address + + // to stay compatible with the old UpdateCustomer code + $billing_values['phone'] = $billing_values['phone_number']; + + $this->user_model->update_billing_address($this->user_model->get('id'), $billing_values); + } + + // deal with shipping address + if ($this->requires_shipping == FALSE) { + $shipping_address = FALSE; + } + elseif ($this->input->post('shipping_address') == 'same') { + $shipping_address = $this->user_model->get_billing_address($this->user_model->get('id')); + } + elseif ($this->input->post('shipping_address') == 'new') { + $shipping_address = $shipping_values; + } + + // save shipping address in session + $this->session->set_userdata('shipping_address', $shipping_address); + + // trigger checkout_billing_shipping + $this->load->library('app_hooks'); + $this->app_hooks->trigger('checkout_billing_shipping'); + + // do we need to redirect to the shipping method selection page? + if ($shipping_address and $this->requires_shipping) { + return redirect('checkout/shipping_method'); + } + else { + return redirect('checkout/payment'); + } + } + + /** + * Choose Shipping Method + */ + function shipping_method () { + $this->require_login(); + $this->get_errors_and_notices(); + $this->prep_cart(); + + // shipping methods? + if ($this->requires_shipping == TRUE) { + $this->load->model('store/shipping_model'); + $shipping_rates = $this->shipping_model->get_rates_for_address($this->cart_model->get_cart(), $this->session->userdata('shipping_address')); + + if (!$shipping_rates) { + // we aren't able to ship to this person's country... damn. + $this->session->set_userdata('errors','Unfortunately, we are unable to ship to the country you selected. Please choose another shipping address. We apologize for the inconvenience.
'); + + return redirect('checkout/billing_shipping'); + } + } + else { + $shipping_rates = FALSE; + } + + $this->smarty->assign('shipping_rates', $shipping_rates); + return $this->smarty->display('checkout_templates/shipping_method.thtml'); + } + + /** + * Post Shipping Method + */ + function post_shipping_method () { + // get available rates + $this->load->model('store/shipping_model'); + $shipping_rates = $this->shipping_model->get_rates_for_address($this->cart_model->get_cart(), $this->session->userdata('shipping_address')); + + if (!array_key_exists($this->input->post('shipping_method'), $shipping_rates)) { + $this->session->set_userdata('errors','You have selected an invalid shipping method. Please select another method.
'); + + return redirect('checkout/shipping_method'); + } + + // we have a shipping rate + $rate = $shipping_rates[$this->input->post('shipping_method')]; + + // get shipping rate to check tax status + $shipping = $this->shipping_model->get_rate($this->input->post('shipping_method')); +//die(''. print_r($rate, true));
+ // save the shipping rate
+ $this->session->set_userdata('shipping_id', $this->input->post('shipping_method'));
+ $this->session->set_userdata('shipping_method', $rate['name']);
+ $this->session->set_userdata('shipping_rate', $rate['total_rate']);
+ $this->session->set_userdata('shipping_is_taxable', $shipping['taxable']);
+
+ // trigger checkout_shipping_method
+ $this->load->library('app_hooks');
+ $this->app_hooks->trigger('checkout_shipping_method');
+
+ return redirect('checkout/payment');
+ }
+
+ /**
+ * Payment
+ */
+ function payment () {
+ $this->require_login();
+ $this->get_errors_and_notices();
+ $this->prep_cart();
+
+ // get cart totals
+ $this->load->model('store/cart_model');
+ $totals = $this->cart_model->calculate_totals();
+
+ if (empty($totals)) {
+ die(show_error('Your cart does not have any products or subscriptions in it. There is nothing to purchase. Go back.'));
+ }
+
+ // redirect to free?
+ if ($this->cart_model->free_cart()) {
+ return redirect('checkout/free_confirm');
+ }
+
+ // get gateways
+ $this->load->model('billing/gateway_model');
+ $gateways = $this->gateway_model->GetGateways(array());
+
+ if (is_array($gateways)) {
+ foreach ($gateways as $key => $gateway) {
+ // is disabled?
+ if ($gateway['enabled'] != TRUE) {
+ unset($gateways[$key]);
+ continue;
+ }
+
+ // get settings
+ $this->load->library('billing/payment/' . $gateway['name']);
+ $settings = $this->$gateway['name']->Settings();
+ $gateways[$key]['external'] = $settings['external'];
+ $gateways[$key]['no_credit_card'] = isset($settings['no_credit_card']) ? $settings['no_credit_card'] : FALSE;
+ }
+ }
+ else {
+ die(show_error('The administrator has not yet setup any payment gateways. These can be configured in the control panel in Configuration > Payment Gateways.'));
+ }
+
+ // month options
+ $month_options = array();
+
+ for ($i = 1; $i <= 12; $i++) {
+ $month = str_pad($i, 2, "0", STR_PAD_LEFT);
+ $month_text = date('M',strtotime('2010-' . $month . '-01'));
+ $month_options[$month] = $month_text;
+ }
+
+ // year options
+ $year_options = array();
+
+ $now = date('Y');
+ $future = $now + 15;
+ for ($i = $now; $i <= $future; $i++) {
+ $year_options[$i] = $i;
+ }
+
+ // do we have a shipping method name?
+ if ($this->session->userdata('shipping_method')) {
+ $shipping_method = $this->session->userdata('shipping_method');
+ }
+ else {
+ $shipping_method = FALSE;
+ }
+
+ // has coupons
+ $this->load->model('coupons/coupon_model');
+ $has_coupons = $this->coupon_model->has_coupons();
+
+ if ($this->session->userdata('active_coupon')) {
+ $coupon = $this->coupon_model->get_coupon($this->session->userdata('active_coupon'));
+ $active_coupon = $coupon['coupon_code'];
+ }
+ else {
+ $active_coupon = FALSE;
+ }
+
+ $this->smarty->assign('active_coupon', $active_coupon);
+ $this->smarty->assign('has_coupons',$has_coupons);
+ $this->smarty->assign('shipping_method', $shipping_method);
+ $this->smarty->assign('month_options', $month_options);
+ $this->smarty->assign('year_options', $year_options);
+ $this->smarty->assign('totals', $totals);
+ $this->smarty->assign('gateways', $gateways);
+ return $this->smarty->display('checkout_templates/payment.thtml');
+ }
+
+ /**
+ * Post Payment
+ */
+ function post_payment () {
+ $this->require_login();
+
+ // calculate totals to make sure we're not getting scammed
+ $this->load->model('store/cart_model');
+ $totals = $this->cart_model->calculate_totals();
+
+ if (empty($totals)) {
+ $this->session->set_userdata('errors','Your cart is empty.
');
+
+ return redirect('checkout/payment');
+ }
+
+ // is this a coupon post?
+ if ($this->input->post('coupon') and $this->input->post('coupon') != '') {
+ $this->load->model('coupons/coupon_model');
+ $coupon = $this->coupon_model->get_coupons(array(
+ 'code' => $this->input->post('coupon')
+ ));
+
+ // no coupon
+ if (empty($coupon)) {
+ $this->session->set_userdata('errors','The coupon code you entered is invalid.
');
+ return redirect('checkout/payment');
+ }
+
+ $coupon = $coupon[0];
+
+ // coupon not yet started
+ if (strtotime($coupon['start_date']) > time()) {
+ $this->session->set_userdata('errors','The coupon code you entered is invalid.
');
+
+ return redirect('checkout/payment');
+ }
+
+ // coupon expired or too many uses
+ if (strtotime($coupon['end_date'])+(60*60*24) < time() or (!empty($coupon['max_uses']) and $coupon['max_uses'] <= $this->coupon_model->count_uses($coupon['id']))) {
+ $this->session->set_userdata('errors','The coupon code you entered has expired.
');
+
+ return redirect('checkout/payment');
+ }
+
+ // one per customer limit?
+ $customer_id = $this->user_model->get_customer_id($this->session->userdata('user_id'));
+ if (!empty($coupon['customer_limit']) and $coupon['customer_limit'] <= $this->coupon_model->customer_usage($coupon['id'], $customer_id)) {
+ $this->session->set_userdata('errors','You have reached the limit for usage of this coupon.
');
+
+ return redirect('checkout/payment');
+ }
+
+ // shipping min. cart amount
+ if ($coupon['type_id'] == '3' and $coupon['min_cart_amt'] > $totals['order_sub_total']) {
+ $this->session->set_userdata('errors','You must have ' . setting('currency_symbol') . money_format($coupon['min_cart_amt']) . ' in your cart to be eligible for free shipping.
');
+
+ return redirect('checkout/payment');
+ }
+
+ // allow for custom coupon validation
+ $this->app_hooks->data('member', $this->user_model->get('id'));
+ $this->app_hooks->trigger('coupon_validate', $coupon['id']);
+
+ // the coupon must be valid and good!
+
+ // let's reset everything in our cart
+ $this->cart_model->reset_to_precoupon();
+
+ // does the coupon get applied?
+ $p_applied = null;
+ $s_applied = null;
+
+ if ($coupon['type_id'] == '1') {
+ // price reduction
+ $is_percentage = $coupon['reduction_type'] == '1' ? FALSE : TRUE;
+
+ // get linked subscriptions/products
+ $subs = $this->coupon_model->get_related($coupon['id'], 'coupons_subscriptions', 'subscription_plan_id');
+ $products = $this->coupon_model->get_related($coupon['id'], 'coupons_products', 'product_id');
+
+ $this->cart_model->reduce_subscription_prices($subs, $coupon['reduction_amt'], $is_percentage);
+ $p_applied = $this->cart_model->reduce_product_prices($products, $coupon['reduction_amt'], $is_percentage);
+ }
+ elseif ($coupon['type_id'] == '2') {
+ // get linked subscriptions/products
+ $subs = $this->coupon_model->get_related($coupon['id'], 'coupons_subscriptions', 'subscription_plan_id');
+
+ $this->cart_model->update_subscription_trial($subs, $coupon['trial_length']);
+ }
+ elseif ($coupon['type_id'] == '3') {
+ $this->session->set_userdata('shipping_rate','0');
+ }
+
+ $this->session->set_userdata('active_coupon', $coupon['id']);
+
+ // Set our default coupon applied message...
+ $this->session->set_userdata('notices','Coupon successfully applied.
');
+
+ // If it didn't match a specific product, change the notice.
+ if ($p_applied === 1)
+ {
+ $this->session->set_userdata('notices','Coupon does not match any products in your cart.
');
+ }
+
+ return redirect('checkout/payment');
+ }
+
+ // get gateway
+ $this->load->model('billing/gateway_model');
+ $gateway = $this->gateway_model->GetGatewayDetails($this->input->post('method'));
+
+ if (empty($gateway)) {
+ $this->session->set_userdata('errors','The payment method you selected is no longer valid.
');
+
+ return redirect('checkout/payment');
+ }
+
+ $this->load->library('billing/payment/' . $gateway['name']);
+ $settings = $this->$gateway['name']->Settings();
+
+ // validate fields
+ if ($settings['no_credit_card'] == FALSE) {
+ $this->load->library('form_validation');
+ $this->form_validation->set_rules('cc_number','Credit Card Number','required');
+ $this->form_validation->set_rules('cc_name','Name on Card','required');
+ $this->form_validation->set_rules('cc_expiry_month','Credit Card Expiry Month','required|is_natural');
+ $this->form_validation->set_rules('cc_expiry_year','Credit Card Expiry Year','required|is_natural');
+
+ if ($this->form_validation->run() === FALSE) {
+ $this->session->set_userdata('errors',validation_errors());
+
+ return redirect('checkout/payment');
+ }
+ }
+
+ // set URL's in case they are using an external gateway
+ $return_url = site_url('checkout/complete');
+ $cancel_url = site_url('checkout/payment');
+
+ // prep credit card array
+ if ($settings['no_credit_card'] == FALSE) {
+ $credit_card = array(
+ 'card_num' => preg_replace('/[^0-9]+/','',$this->input->post('cc_number')),
+ 'name' => $this->input->post('cc_name'),
+ 'exp_month' => $this->input->post('cc_expiry_month'),
+ 'exp_year' => $this->input->post('cc_expiry_year'),
+ 'cvv' => $this->input->post('cc_cvv2')
+ );
+ }
+ else {
+ $credit_card = FALSE;
+ }
+
+ // get customer ID
+ $customer_id = $this->user_model->get_customer_id($this->session->userdata('user_id'));
+
+ // get customer IP
+ $customer_ip = $this->input->ip_address();
+
+ // coupon ID
+ $coupon_id = ($this->session->userdata('active_coupon')) ? $this->session->userdata('active_coupon') : 0;
+
+ // if we don't have a subscription, we'll send a normal Charge
+ if ($this->cart_model->get_subscription_cart() == FALSE) {
+ $response = $this->gateway_model->Charge($gateway['gateway_id'], $totals['order_total'], $credit_card, $customer_id, FALSE, $customer_ip, $return_url, $cancel_url);
+ }
+ else {
+ $subscription = $this->cart_model->get_subscription_cart();
+
+ $this->load->model('billing/subscription_plan_model');
+ $plan = $this->subscription_plan_model->get_plan($subscription['id']);
+
+ $recur = array(
+ 'plan_id' => $plan['plan_id'],
+ 'amount' => $totals['recurring_total'],
+ 'interval' => $subscription['interval'],
+ 'free_trial' => $subscription['free_trial'],
+ 'occurrences' => $subscription['occurrences']
+ );
+
+ $renew = $subscription['renew_subscription_id'];
+
+ $response = $this->gateway_model->Recur($gateway['gateway_id'], $totals['order_total'], $credit_card, $customer_id, FALSE, $customer_ip, $recur, $return_url, $cancel_url, $renew, $coupon_id);
+ }
+
+ // deal with the response (both Charge and Recur have the same response format)
+ if (!is_array($response)) {
+ $this->session->set_userdata('errors','There was an unexpected server error. Please contact the administrators to report your issue.
');
+
+ return redirect('checkout/payment');
+ }
+ elseif (isset($response['error'])) {
+ $this->session->set_userdata('errors','There was an unexpected server error: ' . $response['error_text'] . ' (#' . $response['error'] . ').
');
+
+ return redirect('checkout/payment');
+ }
+ elseif (isset($response['response_code']) and $response['response_code'] != 1 and $response['response_code'] != 100) {
+ $response['reason'] = (!isset($response['reason']) or empty($response['reason'])) ? '' : '. ' . $response['reason'];
+ $this->session->set_userdata('errors','There was an error processing your transaction: ' . $response['response_text'] . $response['reason'] . ' (#' . $response['response_code'] . ')
');
+
+ return redirect('checkout/payment');
+ }
+ else {
+ // success! the charge went through
+
+ // trigger checkout_payment
+ $this->load->library('app_hooks');
+ $this->app_hooks->trigger('checkout_payment');
+
+ $this->process_checkout($response, $totals, $coupon_id);
+ }
+ }
+
+ /**
+ * Free Confirm - The cart is free, just confirm the purchase
+ */
+ function free_confirm () {
+ $this->require_login();
+ $this->get_errors_and_notices();
+
+ // is this cart really free?
+ $this->load->model('store/cart_model');
+ if ($this->cart_model->free_cart() == FALSE) {
+ $this->session->set_userdata('errors','Your cart is not free. You must enter billing information to complete your purchase.');
+
+ return redirect('checkout/billing_shipping');
+ }
+
+ // get cart totals
+ $totals = $this->cart_model->calculate_totals();
+
+ if (empty($totals)) {
+ die(show_error('Your cart does not have any products or subscriptions in it. There is nothing to purchase. Go back.'));
+ }
+
+ $this->smarty->assign('totals', $totals);
+ return $this->smarty->display('checkout_templates/free_confirm.thtml');
+ }
+
+ /**
+ * Post Free Confirm
+ */
+ function post_free_confirm () {
+ $this->require_login();
+
+ // is this cart really free?
+ $this->load->model('store/cart_model');
+ if ($this->cart_model->free_cart() == FALSE) {
+ $this->session->set_userdata('errors','
Your cart is not free. You must enter billing information to complete your purchase.');
+
+ return redirect('checkout/billing_shipping');
+ }
+
+ // get cart totals
+ $totals = $this->cart_model->calculate_totals();
+
+ if (empty($totals)) {
+ $this->session->set_userdata('errors','
Your cart is empty.
');
+
+ return redirect('checkout/free_confirm');
+ }
+
+ // set null information
+ $credit_card = FALSE;
+ $return_url = FALSE;
+ $cancel_url = FALSE;
+
+ // get customer ID
+ $customer_id = $this->user_model->get_customer_id($this->session->userdata('user_id'));
+
+ // get customer IP
+ $customer_ip = $this->input->ip_address();
+
+ // coupon ID
+ $coupon_id = ($this->session->userdata('active_coupon')) ? $this->session->userdata('active_coupon') : 0;
+
+ // use default gateway - it doesn't matter
+ $this->load->model('billing/gateway_model');
+ $gateway = $this->gateway_model->GetGatewayDetails($this->config->item('default_gateway'));
+
+ if (empty($gateway)) {
+ $this->session->set_userdata('errors','There is no default payment gateway enabled to accept this free payment. You must at least create an Offline payment gateway and set it as the default.
');
+
+ return redirect('checkout/free_confirm');
+ }
+
+ $this->load->library('billing/payment/' . $gateway['name']);
+ $settings = $this->$gateway['name']->Settings();
+
+ // if we don't have a subscription, we'll send a normal Charge
+ if ($this->cart_model->get_subscription_cart() == FALSE) {
+ $response = $this->gateway_model->Charge($gateway['gateway_id'], $totals['order_total'], $credit_card, $customer_id, FALSE, $customer_ip, $return_url, $cancel_url);
+ }
+ else {
+ $subscription = $this->cart_model->get_subscription_cart();
+
+ $this->load->model('billing/subscription_plan_model');
+ $plan = $this->subscription_plan_model->get_plan($subscription['id']);
+
+ $recur = array(
+ 'plan_id' => $plan['plan_id'],
+ 'amount' => $totals['recurring_total'],
+ 'interval' => $subscription['interval'],
+ 'free_trial' => $subscription['free_trial'],
+ 'occurrences' => $subscription['occurrences']
+ );
+
+ $renew = $subscription['renew_subscription_id'];
+
+ $response = $this->gateway_model->Recur($gateway['gateway_id'], $totals['order_total'], $credit_card, $customer_id, FALSE, $customer_ip, $recur, $return_url, $cancel_url, $renew, $coupon_id);
+ }
+
+ // deal with the response (both Charge and Recur have the same response format)
+ if (!is_array($response)) {
+ $this->session->set_userdata('errors','There was an unexpected server error. Please contact the administrators to report your issue.
');
+
+ return redirect('checkout/payment');
+ }
+ elseif (isset($response['error'])) {
+ $this->session->set_userdata('errors','There was an unexpected server error: ' . $response['error_text'] . ' (#' . $response['error'] . ').
');
+
+ return redirect('checkout/payment');
+ }
+ elseif (isset($response['response_code']) and $response['response_code'] != 1 and $response['response_code'] != 100) {
+ $response['reason'] = (!isset($response['reason']) or empty($response['reason'])) ? '' : '. ' . $response['reason'];
+ $this->session->set_userdata('errors','There was an error processing your transaction: ' . $response['response_text'] . $response['reason'] . ' (#' . $response['response_code'] . ')
');
+
+ return redirect('checkout/payment');
+ }
+ else {
+ // success! the charge went through
+
+ // trigger checkout_payment
+ $this->load->library('app_hooks');
+ $this->app_hooks->trigger('checkout_payment');
+
+ $this->process_checkout($response, $totals, $coupon_id);
+ }
+ }
+
+ /**
+ * Process Checkout
+ *
+ * This method is called after a free/paid checkout and does various processing things.
+ * For gateways that redirect to external payment, it stores the "charge_id" so that all
+ * post-payment processing occurs when the user comes back to the "Complete" page.
+ * In the complete function, we check to make sure the order status == 1 to stop people from
+ * somehow avoiding checkout and going direct to the complete page.
+ */
+ function process_checkout ($response, $totals, $coupon_id = 0) {
+ $this->load->model('store/taxes_model');
+ $this->load->model('billing/charge_data_model');
+ $this->load->model('store/cart_model');
+
+ // When the user has a $0 products total but a recurring charge,
+ // they will get here without a charge_id because the start_date of their subscription
+ // isn't for X days (after the free trial). This is no good.
+ // Without the charge ID, we can't process their cart.
+ // So, in this case, we will create a charge ID for $0 and add it to the $response...
+ if ((!isset($response['charge_id']) or empty($response['charge_id'])) and $this->cart_model->has_products()) {
+ // create a $0 order for today's payment
+ $this->load->model('billing/charge_model');
+
+ $customer_id = $this->user_model->get_customer_id();
+ $customer_ip = $this->input->ip_address();
+
+ $order_id = $this->charge_model->CreateNewOrder($this->config->item('default_gateway'), 0, array(), $response['recurring_id'], $customer_id, $customer_ip);
+ $this->charge_model->SetStatus($order_id, 1);
+
+ $response['charge_id'] = $order_id;
+ }
+
+ // We don't process the shopping cart, here. We will just record the charge_id in the
+ // user database so that when the charge goes through and the trigger is tripped,
+ // the cart will be processed.
+ // We may not have a charge_id if it's just a subscription and it's delayed, but that means
+ // we don't have any products so that's not a huge deal.
+ if (isset($response['charge_id'])) {
+ $this->user_model->set_charge_id($this->user_model->get('id'), $response['charge_id']);
+
+ // let's save the shipping address and cart totals for processing
+ $this->load->model('billing/charge_data_model');
+
+ if ($this->session->userdata('shipping_address')) {
+ $this->charge_data_model->Save($response['charge_id'], 'shipping_address', serialize($this->session->userdata('shipping_address')));
+ }
+
+ $this->charge_data_model->Save($response['charge_id'], 'totals', serialize($totals));
+ $this->charge_data_model->Save($response['charge_id'], 'coupon_id', $coupon_id);
+ }
+
+ // if there is a recurring charge, we need to record the recurring tax so that future recurring charges will be taxed
+ // the tax for this initial charge will be handled in the store/order_model->process_order() method
+ if (isset($response['recurring_id']) and !empty($totals['tax_id']) and !empty($totals['recurring_tax'])) {
+ $this->taxes_model->future_subscription_tax($response['recurring_id'], $totals['tax_id'], (float)$totals['recurring_tax']);
+ }
+
+ // for future use in the "complete" step, we may want the totals for affiliate integration
+ $this->session->set_userdata('processed_totals', $totals);
+
+ // we're done our stuff - do we need to redirect?
+ if (isset($response['redirect'])) {
+ header('Location: ' . $response['redirect']);
+ return;
+ }
+ else {
+ redirect('checkout/complete');
+ return;
+ }
+ }
+
+ /**
+ * Complete
+ */
+ function complete () {
+ $this->require_login();
+
+ // we need to finish this order
+ if ($this->user_model->get('pending_charge_id')) {
+ // has the order been completed? or are they trying to scam by skipping an external payment?
+ $this->db->where('order_id',$this->user_model->get('pending_charge_id'));
+ $this->db->where('status','1');
+ $result = $this->db->get('orders');
+
+ if ($result->num_rows() == 1) {
+ // process this order
+ $this->load->model('billing/charge_data_model');
+ $charge_data = $this->charge_data_model->Get($this->user_model->get('pending_charge_id'));
+
+ $shipping_address = (isset($charge_data['shipping_address'])) ? unserialize($charge_data['shipping_address']) : FALSE;
+ $totals = unserialize($charge_data['totals']);
+
+ // coupon?
+ $coupon_id = isset($charge_data['coupon_id']) ? $charge_data['coupon_id'] : FALSE;
+
+ // Shipping Name? These are used for dynamic shipping modules only.
+ $shipping_name = $this->session->userdata('shipping_method');
+
+ $this->load->model('store/order_model');
+ $this->order_model->process_order($this->user_model->get('pending_charge_id'), $this->user_model->get('id'), $totals, $shipping_address, $coupon_id, $shipping_name);
+
+ $this->user_model->remove_charge_id($this->user_model->get('id'));
+
+ // clear the cart and those variables
+ $this->session->unset_userdata('shipping_address');
+ $this->session->unset_userdata('totals');
+ $this->session->unset_userdata('cart_contents');
+ $this->session->unset_userdata('shipping_method');
+ $this->session->unset_userdata('shipping_rate');
+
+ $this->db->update('users',array('user_cart' => ''), array('user_id' => $this->user_model->get('id')));
+
+ $this->cart->destroy();
+ }
+ }
+
+ // contains all the totals of the previous transaction, for affiliate integration
+ // these are created prior to the redirect/processing above
+ $totals = $this->session->userdata('processed_totals');
+
+ $this->smarty->assign('totals', $totals);
+ return $this->smarty->display('checkout_templates/complete.thtml');
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/controllers/subscriptions.php b/app/modules/billing/controllers/subscriptions.php
new file mode 100644
index 00000000..7e5690d6
--- /dev/null
+++ b/app/modules/billing/controllers/subscriptions.php
@@ -0,0 +1,96 @@
+load->model('billing/subscription_plan_model');
+ $plans = $this->subscription_plan_model->get_plans(array('sort' => 'subscription_plan_id', 'sort_dir' => 'ASC'));
+
+ $this->smarty->assign('plans', $plans);
+ return $this->smarty->display('subscriptions.thtml');
+ }
+
+ function renew ($subscription_id) {
+ $this->load->model('billing/subscription_model');
+ $subscription = $this->subscription_model->get_subscription($subscription_id);
+
+ if (empty($subscription)) {
+ die(show_error('No subscription exists by that ID.'));
+ }
+
+ if ($subscription['user_id'] != $this->user_model->get('id')) {
+ die(show_error('You do not own the subscription you are trying to renew.'));
+ }
+
+ // check that plan is still active
+ $this->load->model('billing/subscription_plan_model');
+ $plan = $this->subscription_plan_model->get_plan($subscription['plan_id']);
+
+ if (empty($plan)) {
+ die(show_error('The plan you are trying to renew is no longer available.'));
+ }
+
+ // let's renew!
+
+ $this->load->model('store/cart_model');
+ $this->cart_model->add_subscription_to_cart($subscription['plan_id'], $subscription['id']);
+
+ return redirect('checkout');
+ }
+
+ function upgrade ($subscription_id, $new_plan_id) {
+ $this->load->model('billing/subscription_model');
+ $subscription = $this->subscription_model->get_subscription($subscription_id);
+
+ if (empty($subscription)) {
+ die(show_error('No subscription exists by that ID.'));
+ }
+
+ if ($subscription['user_id'] != $this->user_model->get('id')) {
+ die(show_error('You do not own the subscription you are trying to renew.'));
+ }
+
+ // check that plan is active
+ $this->load->model('billing/subscription_plan_model');
+ $plan = $this->subscription_plan_model->get_plan($new_plan_id);
+
+ if (empty($plan)) {
+ die(show_error('The plan you are trying to upgrade to is not available.'));
+ }
+
+ // let's upgrade!
+
+ $this->load->model('store/cart_model');
+ $this->cart_model->add_subscription_to_cart($plan['id'], $subscription['id']);
+
+ return redirect('checkout');
+ }
+
+ function add_to_cart ($subscription_plan_id) {
+ $this->load->model('billing/subscription_plan_model');
+ $plan = $this->subscription_plan_model->get_plan($subscription_plan_id);
+
+ if (empty($plan)) {
+ die(show_error('You have selected an invalid subscription plan.'));
+ }
+
+ $this->load->model('store/cart_model');
+ $this->cart_model->add_subscription_to_cart($plan['id']);
+
+ return redirect('store/cart');
+ }
+}
diff --git a/app/modules/billing/libraries/payment/authnet.php b/app/modules/billing/libraries/payment/authnet.php
new file mode 100644
index 00000000..fc965f59
--- /dev/null
+++ b/app/modules/billing/libraries/payment/authnet.php
@@ -0,0 +1,991 @@
+settings = $this->Settings();
+ }
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'Authorize.net';
+ $settings['class_name'] = 'authnet';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'Authorize.net is the USA\'s premier gateway. Coupled with the powerful Customer Information Manager (CIM), this gateway is an affordable and powerful gateway for any American merchant.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$99.00';
+ $settings['monthly_fee'] = '$40.00';
+ $settings['transaction_fee'] = '$0.10';
+ $settings['purchase_link'] = 'http://www.authorize.net';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 0;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array(
+ 'enabled',
+ 'mode',
+ 'login_id',
+ 'transaction_key',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_dc',
+ 'accept_amex'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'test' => 'Test Mode',
+ 'dev' => 'Development Server'
+ )
+ ),
+ 'login_id' => array(
+ 'text' => 'Login ID',
+ 'type' => 'text'
+ ),
+ 'transaction_key' => array(
+ 'text' => 'Transaction Key',
+ 'type' => 'text'
+ ),
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_dc' => array(
+ 'text' => 'Accept Diner\'s Club?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ function TestConnection($gateway)
+ {
+ $post_url = $this->GetAPIUrl($gateway);
+
+ $post_values = array(
+ "x_login" => $gateway['login_id'],
+ "x_tran_key" => $gateway['transaction_key'],
+ "x_version" => "3.1",
+ "x_delim_data" => "TRUE",
+ "x_delim_char" => "|",
+ "x_relay_response" => "FALSE",
+ "x_type" => "AUTH_CAPTURE",
+ "x_method" => "CC",
+ "x_card_num" => '4222222222222',
+ "x_exp_date" => '1099',
+ "x_amount" => 1,
+ "x_test_request" => TRUE
+ );
+
+ $post_string = "";
+ foreach( $post_values as $key => $value )
+ { $post_string .= "$key=" . urlencode( $value ) . "&"; }
+ $post_string = rtrim( $post_string, "& " );
+
+ $order_id = 0;
+ $response = $this->Process($order_id, $post_url, $post_string, TRUE);
+
+ $CI =& get_instance();
+
+ if($response['success']){
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+
+ return $response;
+ }
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ $post_url = $this->GetAPIUrl($gateway);
+
+ $post_values = array(
+ "x_login" => $gateway['login_id'],
+ "x_tran_key" => $gateway['transaction_key'],
+ "x_version" => "3.1",
+ "x_delim_data" => "TRUE",
+ "x_delim_char" => "|",
+ "x_relay_response" => "FALSE",
+ "x_type" => "AUTH_CAPTURE",
+ "x_method" => "CC",
+ "x_card_num" => $credit_card['card_num'],
+ "x_exp_date" => $credit_card['exp_month'] . '/' . substr($credit_card['exp_year'],-2,2),
+ "x_amount" => $amount,
+ "x_invoice_num" => $order_id
+ );
+
+ if ($gateway['mode'] == 'test') {
+ $post_values['x_test_request'] = 'TRUE';
+ }
+
+ if(isset($credit_card['cvv'])) {
+ $post_values['x_card_code'] = $credit_card['cvv'];
+ }
+
+ if (isset($customer['customer_id'])) {
+ $post_values['x_first_name'] = $customer['first_name'];
+ $post_values['x_last_name'] = $customer['last_name'];
+ $post_values['x_address'] = $customer['address_1'];
+ if (isset($customer['address_2']) and !empty($customer['address_2'])) {
+ $post_values['x_address'] .= ' - '.$customer['address_2'];
+ }
+ $post_values['x_city'] = $customer['city'];
+ $post_values['x_state'] = $customer['state'];
+ $post_values['x_zip'] = $customer['postal_code'];
+ $post_values['x_country'] = $customer['country'];
+ }
+
+ // build NVP post string
+ $post_string = "";
+ foreach($post_values as $key => $value) {
+ $post_string .= "$key=" . urlencode( $value ) . "&";
+ }
+ $post_string = rtrim($post_string, "& ");
+
+ $response = $this->Process($order_id, $post_url, $post_string);
+
+ if ($this->debug)
+ {
+ $this->log_it('Charge Params: ', $post_string);
+ $this->log_it('Charge Response: ', $response);
+ }
+
+ if($response['success']){
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ } else {
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE)
+ {
+ $CI =& get_instance();
+
+ // Create a new authnet profile if one doesn't exist
+ $CI->db->select('api_customer_reference');
+ $CI->db->join('gateways', 'subscriptions.gateway_id = gateways.gateway_id', 'inner');
+ $CI->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ $CI->db->where('api_customer_reference !=','');
+ $CI->db->where('subscriptions.gateway_id',$gateway['gateway_id']);
+ $CI->db->where('subscriptions.active', 1);
+ $CI->db->where('subscriptions.customer_id',$customer['customer_id']);
+ $current_profile = $CI->db->get('subscriptions');
+
+ if ($current_profile->num_rows() > 0) {
+ // save the profile ID
+ $current_profile = $current_profile->row_array();
+ $profile_id = $current_profile['api_customer_reference'];
+
+ // get payment profile to see if a matching one exists
+ $payment_profiles = $this->GetCustomerProfile($profile_id, $gateway);
+
+ if (isset($payment_profiles->profile->paymentProfiles)) {
+ foreach ($payment_profiles->profile->paymentProfiles as $payment_profile) {
+ $card_number = (string)$payment_profile->payment->creditCard->cardNumber;
+
+ if (substr($card_number, -4, 4) == substr($credit_card['card_num'],-4,4)) {
+ $payment_profile_id = (string)$payment_profile->customerPaymentProfileId;
+ $current_payment_profile = $payment_profile;
+ }
+ }
+ }
+ }
+ else {
+ $response = $this->CreateProfile($gateway, $subscription_id, $customer);
+
+ if(isset($response) and !empty($response['success'])) {
+ $profile_id = $response['profile_id'];
+ }
+ }
+
+ if (empty($profile_id)) {
+ $add_text = (isset($response['reason'])) ? $response['reason'] : FALSE;
+ $CI->recurring_model->DeleteRecurring($subscription_id);
+ die($CI->response->Error(5005, $add_text));
+ }
+
+ // save the api_customer_reference
+ $CI->load->model('billing/recurring_model');
+ $CI->recurring_model->SaveApiCustomerReference($subscription_id, $profile_id);
+
+ if (!isset($payment_profile_id) or empty($payment_profile_id)) {
+ // Create the payment profile
+ $response = $this->CreatePaymentProfile($profile_id, $gateway, $credit_card, $customer);
+ if(isset($response) and is_array($response) and isset($response['payment_profile_id'])) {
+ $payment_profile_id = $response['payment_profile_id'];
+ }
+ }
+ else {
+ // We have a payment profile ID, so update the customer's information at AuthNet.
+ $this->UpdateCustomerProfile($profile_id, $payment_profile_id, $gateway, $current_payment_profile, $customer);
+ }
+
+ if (empty($payment_profile_id)) {
+ $add_text = (isset($response['reason'])) ? $response['reason'] : FALSE;
+ $CI->recurring_model->DeleteRecurring($subscription_id);
+ die($CI->response->Error(5006, $add_text));
+ }
+
+ // Save the api_payment_reference
+ $CI->recurring_model->SaveApiPaymentReference($subscription_id, $payment_profile_id);
+
+ // If a payment is to be made today, process it.
+ if ($charge_today === TRUE) {
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ $response = $this->ChargeRecurring($gateway, $order_id, $profile_id, $payment_profile_id, $amount);
+
+ if($response['success'] == TRUE){
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ } else {
+ $response = $CI->response->TransactionResponse(100, array('recurring_id' => $subscription_id));
+ }
+
+ return $response;
+ }
+
+ /*
+ Method: UpdateCustomerProfile()
+
+ Updates the customer's payment profile stored at Authorize.net with the latest
+ customer information.
+
+ Parameters:
+ $profile_id - the customer's profile id.
+ $payment_profile_id - The customer's payment profile id
+ $gateway - The gateway info
+ $orig - AN will blank out any fields that don't have a value, so we need our originals.
+ $new - The new Customer profile information
+ */
+ function UpdateCustomerProfile($profile_id, $payment_profile_id, $gateway, $orig, $new) {
+ $CI =& get_instance();
+
+ $new = (object)$new;
+
+ // Clean up our address into a single entity for transferring to AuthNet
+ if (isset($new->address_2) && !empty($new->address_2))
+ {
+ $new->address_1 = $new->address_1 .', '. $new->address_2;
+ }
+
+ $post_url = $this->GetAPIUrl($gateway, 'arb');
+
+ //--------------------------------------------------------------------
+
+ $company = isset($new->company) ? $new->company : $orig->billTo->company;
+ $address = isset($new->address_1) ? $new->address_1 : $orig->billTo->address;
+ $city = isset($new->city) ? $new->city : $orig->billTo->city;
+ $state = isset($new->state) ? $new->state : $orig->billTo->state;
+ $zip = isset($new->zip) ? $new->zip : $orig->billTo->zip;
+ $country = isset($new->country) ? $new->country : $orig->billTo->country;
+ $phoneNumber = isset($new->phoneNumber) ? $new->phoneNumber : $orig->billTo->phoneNumber;
+
+ //--------------------------------------------------------------------
+
+ $content = '
+
+
+ ' . $gateway['login_id'] . '
+ ' . $gateway['transaction_key'] . '
+
+ ' . $profile_id . '
+
+ ';
+
+ $content .= "{$orig->first_name} ";
+ $content .= "{$orig->last_name} ";
+ $content .= "$company ";
+ $content .= "$address";
+ $content .= "$city ";
+ $content .= "$state ";
+ $content .= "$zip ";
+ $content .= "$phoneNumber ";
+
+ $content .= '
+
+ ';
+
+ $content .= "{$orig->payment->creditCard->cardNumber} ";
+ $content .= "{$orig->payment->creditCard->expirationDate} ";
+
+ $content .= '
+
+ '. $payment_profile_id . '
+
+ ';
+
+ if ($this->debug)
+ {
+ $this->log_it('UpdateCustomerProfile Params: ', $content);
+ }
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
+ curl_setopt($request, CURLOPT_POSTFIELDS, $content); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close($request); // close curl object
+
+ if ($this->debug)
+ {
+ $this->log_it('UpdateCustomerProfile Response: ', $post_response);
+ }
+ }
+
+ function Refund ($gateway, $charge, $authorization)
+ {
+ $CI =& get_instance();
+
+ $post_url = $this->GetAPIUrl($gateway);
+
+ $post_values = array(
+ "x_login" => $gateway['login_id'],
+ "x_tran_key" => $gateway['transaction_key'],
+ "x_version" => "3.1",
+ "x_delim_data" => "TRUE",
+ "x_delim_char" => "|",
+ "x_relay_response" => "FALSE",
+ "x_type" => "CREDIT",
+ "x_method" => "CC",
+ "x_trans_id" => $authorization->tran_id,
+ "x_amount" => $charge['amount'],
+ "x_card_num" => $charge['card_last_four'],
+ "x_exp_date" => date('m') . substr((date('Y')+1),-2,2)
+ );
+
+ $post_string = "";
+ foreach( $post_values as $key => $value ) {
+ $post_string .= "$key=" . urlencode( $value ) . "&";
+ }
+
+ $post_string = rtrim( $post_string, "& " );
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_POSTFIELDS, $post_string); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+ curl_close ($request); // close curl object
+
+ $response = explode('|',$post_response);
+
+ if (!isset($response[1])) {
+ // the array is malformed
+ return FALSE;
+ }
+
+ if ($response[0] == '1') {
+ return TRUE;
+ }
+ else {
+ // let's try a VOID - this may be unsettled
+ $post_values = array(
+ "x_login" => $gateway['login_id'],
+ "x_tran_key" => $gateway['transaction_key'],
+ "x_version" => "3.1",
+ "x_delim_data" => "TRUE",
+ "x_delim_char" => "|",
+ "x_relay_response" => "FALSE",
+ "x_type" => "VOID",
+ "x_method" => "CC",
+ "x_trans_id" => $authorization->tran_id,
+ "x_amount" => $charge['amount'],
+ "x_card_num" => $charge['card_last_four'],
+ "x_exp_date" => date('m') . substr((date('Y')+1),-2,2)
+ );
+
+ $post_string = "";
+ foreach( $post_values as $key => $value ) {
+ $post_string .= "$key=" . urlencode( $value ) . "&";
+ }
+
+ $post_string = rtrim( $post_string, "& " );
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_POSTFIELDS, $post_string); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+ curl_close ($request); // close curl object
+
+ $response = explode('|',$post_response);
+
+ if (!isset($response[1])) {
+ // array is malford
+ return FALSE;
+ }
+ else {
+ if ($response[1] == '1') {
+ // it was voided successfully
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ function CreateProfile($gateway, $subscription_id, $customer = array())
+ {
+ $CI =& get_instance();
+
+ $post_url = $this->GetAPIUrl($gateway, 'arb');
+
+ $email = (isset($customer['email']) and !empty($customer['email'])) ? "" . $customer['email'] . " " : '';
+
+ $content =
+ "" .
+ "".
+ "
+ ".$gateway['login_id']."
+ ".$gateway['transaction_key']."
+
+ ".
+ "".
+ "".$subscription_id." ".
+ $email.
+ " ".
+ " ";
+
+ if ($this->debug)
+ {
+ $this->log_it('CreateProfile Params: ', $content);
+ }
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
+ curl_setopt($request, CURLOPT_POSTFIELDS, $content); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close($request); // close curl object
+
+ @$response = simplexml_load_string($post_response);
+
+ $return = array();
+
+ if ($this->debug)
+ {
+ $this->log_it('CreateProfile Response: ', $response);
+ }
+
+ if($response->messages->resultCode == 'Ok') {
+ $return['success'] = TRUE;
+ $return['profile_id'] = (string)$response->customerProfileId;
+ } else {
+ $return['success'] = FALSE;
+ $return['reason'] = (string)$response->messages->message->text;
+ }
+
+ return $return;
+ }
+
+ function CreatePaymentProfile($profile_id, $gateway, $credit_card, $customer)
+ {
+ $CI =& get_instance();
+
+ $post_url = $this->GetAPIUrl($gateway, 'arb');
+
+ $first_name = $customer['first_name'];
+ $last_name = $customer['last_name'];
+
+ if (!empty($customer['address_1']) and strlen($customer['state']) == 2) {
+ $customer_details = "" . $customer['company'] . " ".
+ "" . $customer['address_1'] . "".
+ "" . $customer['city'] . " ".
+ "" . $customer['state'] . " ".
+ "" . $customer['postal_code'] . " ";
+ if (isset($customer['country']) && ($customer['country'] == 'US' || $customer['country'] == 'CA'))
+ {
+ $customer_details .= "" . $customer['country'] . " ";
+ }
+ $customer_details .= "" . $customer['phone'] . " ";
+ }
+ else {
+ $customer_details = '';
+ }
+
+ $card_code = (isset($credit_card['cvv'])) ? "" . $credit_card['cvv'] . ' ' : '';
+
+ $content =
+ "" .
+ "" .
+ "
+ ".$gateway['login_id']."
+ ".$gateway['transaction_key']."
+
+ ".
+ "" . $profile_id . " ".
+ "".
+ "".
+ "".$first_name." ".
+ "".$last_name." ".
+ $customer_details.
+ " ".
+ "".
+ "".
+ "".$credit_card['card_num']." ".
+ "".str_pad($credit_card['exp_year'], 4, "20", STR_PAD_LEFT)."-".str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT)." ". // required format for API is YYYY-MM
+ $card_code.
+ " ".
+ " ".
+ "
+ ";
+
+ if ($this->debug)
+ {
+ $this->log_it('CreatePaymentProfile Params: ', $content);
+ }
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
+ curl_setopt($request, CURLOPT_POSTFIELDS, $content); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close($request); // close curl object
+
+ @$response = simplexml_load_string($post_response);
+
+ if ($this->debug)
+ {
+ $this->log_it('CreatePaymentProfile Response: ', $response);
+ }
+
+ $return = array();
+
+ if($response->messages->resultCode == 'Ok') {
+ $return['success'] = TRUE;
+ $return['payment_profile_id'] = (string)$response->customerPaymentProfileId;
+ } else {
+ $return['success'] = FALSE;
+ $return['reason'] = (string)$response->messages->message->text;
+ }
+
+ return $return;
+ }
+
+ function GetCustomerProfile ($profile_id, $gateway) {
+ $CI =& get_instance();
+
+ $post_url = $this->GetAPIUrl($gateway, 'arb');
+
+ $content = '
+
+
+ ' . $gateway['login_id'] . '
+ ' . $gateway['transaction_key'] . '
+
+ ' . $profile_id . '
+ ';
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
+ curl_setopt($request, CURLOPT_POSTFIELDS, $content); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close($request); // close curl object
+
+ @$response = simplexml_load_string($post_response);
+
+ return $response;
+ }
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $order_id, $params['api_customer_reference'], $params['api_payment_reference'], $params['amount']);
+ }
+
+ function ChargeRecurring($gateway, $order_id, $profile_id, $payment_profile_id, $amount)
+ {
+ $CI =& get_instance();
+
+ $post_url = $this->GetAPIUrl($gateway, 'arb');
+
+ $content =
+ "" .
+ "" .
+ "
+ ".$gateway['login_id']."
+ " . $gateway['transaction_key'] . "
+ ".
+ "".
+ "".
+ "" . $amount . " ".
+ "" . $profile_id . " ".
+ "" . $payment_profile_id . " ".
+ "".
+ "".$order_id." ".
+ " ".
+ " ".
+ "
+ ";
+
+ if ($this->debug)
+ {
+ $this->log_it('ChargeRecurring Params: ', $content);
+ }
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
+ curl_setopt($request, CURLOPT_POSTFIELDS, $content); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close($request); // close curl object
+
+ @$post_response = simplexml_load_string($post_response);
+
+ if ($this->debug)
+ {
+ $this->log_it('ChargeRecurring Response: ', $post_response);
+ }
+
+ if($post_response->messages->resultCode == 'Ok') {
+ // Get the auth code
+ $post_response = explode(',', $post_response->directResponse);
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $post_response[6], $post_response[4]);
+ $response['success'] = TRUE;
+ } else {
+ $response['success'] = FALSE;
+ $response['reason'] = (string)$post_response->messages->message->text[0];
+ }
+
+ return $response;
+ }
+
+ function UpdateRecurring()
+ {
+ return TRUE;
+ }
+
+ function Process($order_id, $post_url, $post_string, $test = FALSE)
+ {
+ $CI =& get_instance();
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_POSTFIELDS, $post_string); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($request, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close ($request); // close curl object
+
+ $response = explode('|',$post_response);
+
+ if(!isset($response[1])) {
+ $response['success'] = FALSE;
+ return $response;
+ }
+
+ if($test) {
+ if($response[0] == 1) {
+ $response['success'] = TRUE;
+ } else {
+ $response['success'] = FALSE;
+ }
+
+ return $response;
+ }
+ // Get the response. 1 for the first part meant that it was successful. Anything else and it failed
+ if ($response[0] == 1) {
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response[6], $response[4]);
+ $CI->charge_model->SetStatus($order_id, 1);
+
+ $response['success'] = TRUE;
+ } else {
+ $CI->load->model('billing/charge_model');
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response['success'] = FALSE;
+ $response['reason'] = $response[3];
+ }
+
+ return $response;
+
+ }
+
+ function GetAPIUrl ($gateway, $mode = FALSE) {
+ if ($mode == FALSE) {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['url_dev'];
+ break;
+ }
+ }
+ elseif ($mode == 'arb') {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['arb_url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['arb_url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['arb_url_dev'];
+ break;
+ }
+ }
+
+ return $post_url;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: log_it()
+
+ Logs the transaction to a file. Helpful with debugging callback
+ transactions, since we can't actually see what's going on.
+
+ Parameters:
+ $heading - A string to be placed above the resutls
+ $params - Typically an array to print_r out so that we can inspect it.
+ */
+ public function log_it($heading, $params)
+ {
+ $file = FCPATH .'writeable/gateway_log.txt';
+
+ $content = '';
+ $content .= "\n\n//---------------------------------------------------------------\n";
+ $content .= "\n\n[{$this->settings['name']}] $heading\n";
+ $content .= date('Y-m-d H:i:s') ."\n\n";
+ $content .= print_r($params, true);
+ file_put_contents($file, $content, FILE_APPEND);
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ function Auth($order_id, $gateway, $customer, $params, $credit_card)
+ {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['url_dev'];
+ break;
+ }
+
+ $post_values = array(
+ "x_login" => $gateway['login_id'],
+ "x_tran_key" => $gateway['transaction_key'],
+ "x_version" => "3.1",
+ "x_delim_data" => "TRUE",
+ "x_delim_char" => "|",
+ "x_relay_response" => "FALSE",
+ "x_type" => "AUTH_ONLY",
+ "x_method" => "CC",
+ "x_card_num" => $credit_card['card_num'],
+ "x_exp_date" => $credit_card['exp_month'].$credit_card['exp_year'],
+ "x_amount" => $params['amount']
+ );
+
+ if(isset($credit_card->cvv)) {
+ $post_values['x_card_code'] = $credit_card['cvv'];
+ }
+
+ if(isset($params['customer_id'])) {
+ $post_values['x_first_name'] = $customer['first_name'];
+ $post_values['x_last_name'] = $customer['last_name'];
+ $post_values['x_address'] = $customer['address_1'].'-'.$customer['address_2'];
+ $post_values['x_state'] = $customer['state'];
+ $post_values['x_zip'] = $customer['postal_code'];
+ }
+
+ if(isset($params['description'])) {
+ $post_values['x_description'] = $params['description'];
+ }
+
+ $post_string = "";
+ foreach( $post_values as $key => $value )
+ { $post_string .= "$key=" . urlencode( $value ) . "&"; }
+ $post_string = rtrim( $post_string, "& " );
+
+ $response = $this->Process($order_id, $post_url, $post_string);
+
+ $CI =& get_instance();
+
+ if($response['success']){
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ } else {
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ function Capture($order_id, $gateway, $customer, $params)
+ {
+ $CI =& get_instance();
+
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['url_dev'];
+ break;
+ }
+
+ // Get the tran id
+ $CI->load->model('order_authorization_model');
+ $order = $CI->order_authorization_model->GetAuthorization($order_id);
+
+ $post_values = array(
+ "x_login" => $gateway['login_id'],
+ "x_tran_key" => $gateway['transaction_key'],
+
+ "x_version" => "3.1",
+ "x_delim_data" => "TRUE",
+ "x_delim_char" => "|",
+ "x_relay_response" => "FALSE",
+
+ "x_type" => "PRIOR_AUTH_CAPTURE",
+ "x_method" => "CC",
+ "x_tran_id" => $order->tran_id
+ );
+
+ $post_string = "";
+ foreach( $post_values as $key => $value )
+ { $post_string .= "$key=" . urlencode( $value ) . "&"; }
+ $post_string = rtrim( $post_string, "& " );
+
+ $response = $this->Process($order_id, $post_url, $post_string);
+
+ if($response['success']){
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ } else {
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }*/
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/eway.php b/app/modules/billing/libraries/payment/eway.php
new file mode 100644
index 00000000..e50a423a
--- /dev/null
+++ b/app/modules/billing/libraries/payment/eway.php
@@ -0,0 +1,652 @@
+settings = $this->Settings();
+ }
+
+ //---------------------------------------------------------------
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'eWAY';
+ $settings['class_name'] = 'eway';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'eWAY is the premier gateway solution in Australia.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$0';
+ $settings['monthly_fee'] = '$29';
+ $settings['transaction_fee'] = '$0.50';
+ $settings['purchase_link'] = 'https://www.eway.com.au/join/secure/signup.aspx';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 0;
+ $settings['requires_customer_information'] = 1;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array(
+ 'enabled',
+ 'mode',
+ 'customer_id',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_dc',
+ 'accept_amex',
+ 'username',
+ 'password'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'test' => 'Test Mode',
+ 'dev' => 'Development Server'
+ )
+ ),
+ 'customer_id' => array(
+ 'text' => 'Login ID',
+ 'type' => 'text'
+ ),
+
+ 'username' => array(
+ 'text' => 'Rebill Username',
+ 'type' => 'text'
+ ),
+
+ 'password' => array(
+ 'text' => 'Rebill Password',
+ 'type' => 'password'
+ ),
+
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_dc' => array(
+ 'text' => 'Accept Diner\'s Club?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ //---------------------------------------------------------------
+
+ /**
+ * Tests that the user information provided is a valid set of rules.
+ * We do this by creating a new test customer on the remote server.
+ *
+ * @param array $gateway - the gateway object.
+ * @return bool true if client info appears correct.
+ */
+ function TestConnection($gateway)
+ {
+ $customer = array(
+ 'Title' => 'Mr.',
+ 'FirstName' => 'Joe',
+ 'LastName' => 'Bloggs',
+ 'Address' => 'Blogg enterprises',
+ 'Suburb' => 'Capital City',
+ 'State' => 'act',
+ 'Company' => 'Bloggs',
+ 'PostCode' => '2111',
+ 'Country' => 'au',
+ 'Email' => 'test@eway.com.au',
+ 'Fax' => '0298989898',
+ 'Phone' => '0297979797',
+ 'Mobile' => '',
+ 'CustomerRef'=> 'Ref123',
+ 'JobDesc' => '',
+ 'Comments' => 'Please ship ASAP',
+ 'URL' => 'http://www.test.com.au',
+ 'CCNumber' => '4444333322221111',
+ 'CCNameOnCard' => 'Test Account',
+ 'CCExpiryMonth' => '12',
+ 'CCExpiryYear' => date('y')
+ );
+
+ $response = $this->processSoap($gateway, $customer, 'CreateCustomer');
+
+ if (isset($response['CREATECUSTOMERRESULT']))
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ //---------------------------------------------------------------
+
+ //---------------------------------------------------------------
+ // !CUSTOMER FUNCTIONS
+ //---------------------------------------------------------------
+
+ function createProfile($gateway, $customer, $credit_card, $subscription_id, $amount, $order_id)
+ {
+ $CI =& get_instance();
+
+ $xml = array(
+ 'Title' => isset($customer['title']) && !empty($customer['title']) ? $customer['title'] : 'Mr.',
+ 'FirstName' => $customer['first_name'],
+ 'LastName' => $customer['last_name'],
+ 'Address' => $customer['address_1'],
+ 'Suburb' => $customer['city'],
+ 'State' => $customer['state'],
+ 'Company' => $customer['company'],
+ 'PostCode' => $customer['postal_code'],
+ 'Country' => strtolower($customer['country']),
+ 'Email' => $customer['email'],
+ 'Fax' => 'N/A',
+ 'Phone' => 'N/A',
+ 'Mobile' => 'N/A',
+ 'CustomerRef'=> $order_id,
+ 'JobDesc' => 'N/A',
+ 'Comments' => 'N/A',
+ 'URL' => '',
+ 'CCNumber' => isset($credit_card['card_num']) ? $credit_card['card_num'] : '',
+ 'CCNameOnCard' => isset($credit_card['name']) ? $credit_card['name'] : '',
+ 'CCExpiryMonth' => isset($credit_card['exp_month']) ? $credit_card['exp_month'] : '',
+ 'CCExpiryYear' => isset($credit_card['exp_year']) ? substr($credit_card['exp_year'], -2, 2) : ''
+ );
+
+ $response = $this->processSoap($gateway, $xml, 'CreateCustomer');
+
+ if(isset($response['CREATECUSTOMERRESULT']) && is_numeric($response['CREATECUSTOMERRESULT']))
+ {
+ $response['success'] = true;
+ $response['client_id'] = $response['CREATECUSTOMERRESULT'];
+ // Save the Auth information
+ $CI->load->model('billing/recurring_model');
+ $CI->recurring_model->SaveApiCustomerReference($subscription_id, $response['CREATECUSTOMERRESULT']);
+ // Client successfully created at eWay. Now we ned to save the info here.
+ return $response;
+ }
+ else
+ {
+ $response['success'] = false;
+ $response['reason'] = 'Could not create customer at eWay.';
+ return $response;
+ }
+ }
+
+ //---------------------------------------------------------------
+
+ //---------------------------------------------------------------
+ // !EVENT FUNCTIONS
+ //---------------------------------------------------------------
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ // The Charge function is the only one to use the eWay Hosted Payments solution,
+ // so the url's are not stored in the database. Instead, they are provided here.
+ $post_url = $gateway['mode'] == 'prod' ? 'https://www.eway.com.au/gateway/xmlpayment.asp' : 'https://www.eway.com.au/gateway/xmltest/testpage.asp';
+
+ $post['ewaygateway']['ewayCustomerID'] = $gateway['customer_id'];
+ $post['ewaygateway']['ewayTotalAmount'] = number_format($amount,2,'','');
+
+ $post['ewaygateway']['ewayCardNumber'] = $credit_card['card_num'];
+ $post['ewaygateway']['ewayCardExpiryMonth'] = $credit_card['exp_month'];
+ $post['ewaygateway']['ewayCardExpiryYear'] = substr($credit_card['exp_year'],-2,2);
+ $post['ewaygateway']['ewayTrxnNumber'] = '';
+ $post['ewaygateway']['ewayOption1'] = '';
+ $post['ewaygateway']['ewayOption2'] = '';
+ $post['ewaygateway']['ewayOption3'] = '';
+ $post['ewaygateway']['ewayCustomerInvoiceDescription'] = '';
+ $post['ewaygateway']['ewayCustomerInvoiceRef'] = $order_id;
+
+ $post['ewaygateway']['ewayCardHoldersName'] = $customer['first_name'].' '.$customer['last_name'];
+ $post['ewaygateway']['ewayCustomerFirstName'] = $customer['first_name'];
+ $post['ewaygateway']['ewayCustomerLastName'] = $customer['last_name'];
+ $post['ewaygateway']['ewayCustomerAddress'] = $customer['address_1'];
+ if (isset($customer['address_2']) and !empty($customer['address_2'])) {
+ $post['ewaygateway']['ewayCustomerAddress'] .= ' - '.$customer['address_2'];
+ }
+ $post['ewaygateway']['ewayCustomerPostcode'] = $customer['postal_code'];
+ $post['ewaygateway']['ewayCustomerEmail'] = $customer['email'];
+
+ if(isset($credit_card['cvv'])) {
+ $post['ewaygateway']['ewayCVN'] = $credit_card['cvv'];
+ }
+
+ $CI->load->library('array_to_xml');
+ $xml = $CI->array_to_xml->toXml($post);
+
+ $xml = str_replace('','', $xml);
+ $xml = str_replace(' ','', $xml);
+
+ $response = $this->Process($post_url,$xml);
+
+ if($response['ewayTrxnStatus'] == 'True')
+ {
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['ewayTrxnNumber'], $response['ewayAuthCode']);
+ $CI->charge_model->SetStatus($order_id, 1);
+
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ }
+ else
+ {
+ $CI->load->model('billing/charge_model');
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response_array = array('reason' => $response['ewayTrxnError']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ //---------------------------------------------------------------
+
+ /**
+ * Recur - called when an initial Recur charge comes through to
+ * to create a subscription.
+ *
+ * Since we're using the TokenAPI and not an actual recurring api
+ * this method's primary purpose is to simply setup a user and charge
+ * them through the Token aPI.
+ */
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE)
+ {
+ $CI =& get_instance();
+
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $customer['customer_id'] = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ // Create the recurring seed
+ $response = $this->CreateProfile($gateway, $customer, $credit_card, $subscription_id, $amount, $order_id);
+
+ // Process today's payment
+ if ($charge_today === TRUE) {
+ if ($gateway['mode'] != 'live') $response['client_id'] = '9876543211000';
+
+ $response = $this->ChargeRecurring($gateway, $order_id, $response['client_id'], $amount);
+
+ if($response['success'] == TRUE){
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ } else {
+ $response = $CI->response->TransactionResponse(100, array('recurring_id' => $subscription_id));
+ }
+
+ return $response;
+ }
+
+ //---------------------------------------------------------------
+
+ function CancelRecurring($subscription)
+ {
+ // Recurring not stored at eWay, so do nothing here.
+ return TRUE;
+ }
+
+ //---------------------------------------------------------------
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $order_id, $params['api_customer_reference'], $params['amount']);
+ }
+
+ //-----------------------------------------------------
+
+
+ /**
+ * Handles paying the recurring charge.
+ *
+ * NOTE: eWay does NOT provide a method in their API to trigger this,
+ * it appears that eWay handles this automatically, so we are using
+ * the TokenAPI.
+ */
+ function ChargeRecurring ($gateway, $order_id, $customer_id, $amount) {
+ $CI =& get_instance();
+
+ if ($gateway['mode'] != 'live') $customer_id = '9876543211000';
+
+ $xml = array(
+ 'managedCustomerID' => $customer_id,
+ 'amount' => number_format($amount, 2, '', ''),
+ 'invoiceReference' => $order_id,
+ 'invoiceDescription'=> 'Recurring Payment.'
+ );
+
+ $response = $this->processSoap($gateway, $xml, 'ProcessPayment');
+
+ if (isset($response['EWAYTRXNSTATUS']) && $response['EWAYTRXNSTATUS'] == 'True')
+ {
+ $response['success'] = TRUE;
+ $response['transaction_num'] = $response['EWAYTRXNNUMBER'];
+ $response['auth_code'] = $response['EWAYAUTHCODE'];
+
+ // Save the Auth information
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['EWAYTRXNNUMBER'], $response['EWAYAUTHCODE']);
+ } else
+ {
+ $response['success'] = FALSE;
+ $response['reason'] = $response['EWAYTRXNERROR'];
+ }
+
+ return $response;
+ }
+
+ //---------------------------------------------------------------
+
+ function UpdateRecurring($gateway, $subscription, $customer, $params)
+ {
+ // Recurring info not stored at eWay, so do nothing here...
+ return TRUE;
+ }
+
+ //---------------------------------------------------------------
+
+ //---------------------------------------------------------------
+ // !PROCESSORS
+ //---------------------------------------------------------------
+
+ function Process($url, $xml)
+ {
+ $ch = curl_init($url); // initiate curl object
+ curl_setopt($ch, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); // use HTTP POST to send form data
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: text/xml;charset=UTF-8'));
+
+ // We need to make curl recognize the CA certificated so it can get by...
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+
+ $post_response = curl_exec($ch); // execute curl post and store results in $post_response
+
+ if (curl_errno($ch) == CURLE_OK)
+ {
+ $response_xml = @simplexml_load_string($post_response);
+ $CI =& get_instance();
+ $CI->load->library('array_to_xml');
+ $response = $CI->array_to_xml->toArray($response_xml);
+
+ return $response;
+ }
+
+ return FALSE;
+
+ }
+
+ //---------------------------------------------------------------
+
+ /**
+ * ProcessSoap()
+ *
+ * Uses the SOAP protocol to talk with eway. Sends a secure header
+ *
+ */
+ function processSoap($gateway, $xml, $action)
+ {
+ $CI =& get_instance();
+ $CI->load->library('array_to_xml');
+
+ $url = $this->GetAPIUrl($gateway, 'rebill');
+
+ $header = '
+
+
+
+ '. $gateway['customer_id'] .'
+ '. $gateway['username'] .'
+ '. $gateway['password'] .'
+
+ ';
+
+ $body = ''. $this->to_xml($xml, $action, 'man') . ' ';
+
+ $request = trim($header) . trim($body);
+
+ // Send the request via CURL
+ $ch = curl_init($url); // initiate curl object
+ curl_setopt($ch, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $request); // use HTTP POST to send form data
+ curl_setopt ($ch, CURLOPT_HTTPHEADER, array('SOAPAction: https://www.eway.com.au/gateway/managedpayment/'.$action, 'Content-type: text/xml'));
+
+ // We need to make curl ignore the CA certificate so it can get by...
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+
+ $post_response = curl_exec($ch); // execute curl post and store results in $post_response
+
+ if (curl_errno($ch) == CURLE_OK)
+ {
+ // No CURL Errors, so translate the returned XML into a usable object to return
+ $p = xml_parser_create(); // Create a parser
+ xml_parse_into_struct($p, trim($post_response), $response, $index); // Parse into a $response array
+
+ // Any errors?
+ if (xml_get_error_code($p) != XML_ERROR_NONE)
+ {
+ xml_parser_free($p); // Free the parser
+ return false;
+ }
+ xml_parser_free($p); // Free the parser
+
+ // Combine into an organized array...
+ $response = $this->format_xml_array($response, $index);
+
+ return $response;
+ } else
+ {
+ echo 'Curl Error Number: '. curl_errno($ch) .', Error: '. curl_error($ch);
+ }
+
+ return FALSE;
+
+ }
+
+ //---------------------------------------------------------------
+
+ //---------------------------------------------------------------
+
+ //---------------------------------------------------------------
+ // !UTILITY FUNCTIONS
+ //---------------------------------------------------------------
+
+ /**
+ * Returns the proper url for the remote gateway.
+ *
+ * Note that $mode param defaults to false, which will
+ * return the token payments url. If $mode is 'arb',
+ * then it will return the rebill url.
+ */
+ function GetAPIUrl ($gateway, $mode = FALSE) {
+ if ($mode == FALSE) {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['url_dev'];
+ break;
+ }
+ }
+ elseif ($mode == 'rebill') {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ //$post_url = $gateway['arb_url_live'];
+ $post_url = 'https://www.eway.com.au/gateway/ManagedPaymentService/managedCreditCardPayment.asmx';
+ break;
+ case 'test':
+ $post_url = $gateway['arb_url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['arb_url_dev'];
+ break;
+ }
+ }
+
+ return $post_url;
+ }
+
+ //---------------------------------------------------------------
+
+ public function aus_date($time=null, $format='d/m/Y')
+ {
+ // Use today's date as the timestamp if none given.
+ if (!is_numeric($time))
+ {
+ $time = time();
+ }
+
+ if (!function_exists('local_to_gmt'))
+ {
+ $this->load->helper('date');
+ }
+
+ // Standardize time to GMT
+ $time = local_to_gmt($time);
+
+ // Convert to Australian time
+ $time = gmt_to_local($time, 'UP10');
+
+ return date($format, $time);
+ }
+
+ //---------------------------------------------------------------
+
+ public function to_xml($array=array(), $action='', $ns='man')
+ {
+ if (!count($array) || empty($action))
+ {
+ return false;
+ }
+
+ $xml = "<$ns:$action>\n";
+
+ foreach ($array as $key => $value)
+ {
+ $xml .= "\t<$ns:$key>$value$ns:$key>\n";
+ }
+
+ $xml .= "$ns:$action>\n";
+
+ return $xml;
+ }
+
+ //---------------------------------------------------------------
+
+ private function format_xml_array($orig, $index)
+ {
+ $response = array();
+
+ foreach ($index as $key => $values)
+ {
+ $response[$key] = isset($orig[$values[0]]['value']) ? $orig[$values[0]]['value'] : '';
+ }
+
+ return $response;
+ }
+
+ //---------------------------------------------------------------
+
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/exact.php b/app/modules/billing/libraries/payment/exact.php
new file mode 100644
index 00000000..ccf3ad2f
--- /dev/null
+++ b/app/modules/billing/libraries/payment/exact.php
@@ -0,0 +1,483 @@
+settings = $this->Settings();
+ }
+
+ //--------------------------------------------------------------------
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'E-xact';
+ $settings['class_name'] = 'exact';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'E-xact from VersaPay is the perfect gateway for both Canadian and American merchants.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$149.99';
+ $settings['monthly_fee'] = '$29.99';
+ $settings['transaction_fee'] = '$0.25';
+ $settings['purchase_link'] = 'http://ecommerce.versapay.com/';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 0;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array('enabled',
+ 'terminal_id',
+ 'password',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_dc',
+ 'accept_amex');
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'terminal_id' => array(
+ 'text' => 'Gateway ID',
+ 'type' => 'text'
+ ),
+ 'password' => array(
+ 'text' => 'Password',
+ 'type' => 'password'
+ ),
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_dc' => array(
+ 'text' => 'Accept Diner\'s Club?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ //--------------------------------------------------------------------
+
+ function TestConnection($gateway)
+ {
+ $post_url = $gateway['url_live'];
+
+ $trxnProperties = array(
+ 'ExactID' => $gateway['terminal_id'],
+ 'Password' => $gateway['password'],
+ 'Transaction_Type' => '00',
+ 'Card_Number' => '4222222222222222',
+ 'Expiry_Date' => '1099',
+ 'CVD_Presence_Ind' => '9',
+ 'DollarAmount' => 1
+ );
+
+ $trxnProperties = $this->CompleteArray($trxnProperties);
+
+ $trxnResult = $this->Process($trxnProperties, $post_url);
+
+ if (isset($trxnResult->ExactID)) {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ $post_url = $gateway['url_live'];
+
+ $transaction = array(
+ 'User_Name' => '',
+ 'ExactID' => $gateway['terminal_id'],
+ 'Password' => $gateway['password'],
+ 'Transaction_Type' => '00',
+ 'Card_Number' => $credit_card['card_num'],
+ 'Expiry_Date' => str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . substr($credit_card['exp_year'],-2,2),
+ 'CVD_Presence_Ind' => '1',
+ 'Customer_Ref' => $order_id,
+ 'DollarAmount' => $amount
+ );
+
+ if (isset($credit_card['cvv'])) {
+ $transaction['VerificationStr2'] = $credit_card['cvv'];
+ }
+
+ if (isset($customer['customer_id'])) {
+ // build customer's name from customer array
+ $transaction['CardHoldersName'] = $customer['first_name'].' '.$customer['last_name'];
+ }
+ else {
+ // automatically get customer's name from credit card
+ $name = explode(' ', $credit_card['name']);
+ $transaction['CardHoldersName'] = $name[0] . ' ' . $name[1];
+ }
+
+ if (isset($customer['ip_address']) and !empty($customer['ip_address'])) {
+ $transaction['Client_IP'] = $customer['ip_address'];
+ }
+
+ $transaction = $this->CompleteArray($transaction);
+
+ $transaction_result = $this->Process($transaction, $post_url);
+
+ if($transaction_result->EXact_Resp_Code == '00' and $transaction_result->Transaction_Approved === TRUE){
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $transaction_result->Transaction_Tag, $transaction_result->Authorization_Num);
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ } else {
+ $response_array = array('reason' => $transaction_result->EXact_Message);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE)
+ {
+ $CI =& get_instance();
+
+ // Create an order for today's (potential) payment
+ $CI->load->model('billing/charge_model');
+ $customer['customer_id'] = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ // Create the recurring seed
+ $response = $this->CreateProfile($gateway, $customer, $credit_card, $subscription_id, $amount, $order_id);
+
+ if ($response['success'] == TRUE) {
+ // Process today's payment
+ if ($charge_today === TRUE) {
+ $response = $this->ChargeRecurring($gateway, $order_id, $response['transaction_tag'], $response['auth_num'], $amount);
+
+ if($response['success'] == TRUE){
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ } else {
+ $response = $CI->response->TransactionResponse(100, array('recurring_id' => $subscription_id));
+ }
+ }
+ else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function Refund ($gateway, $charge, $authorization)
+ {
+ $CI =& get_instance();
+
+ $post_url = $gateway['url_live'];
+
+ $trxnProperties = array(
+ 'ExactID' => $gateway['terminal_id'],
+ 'Password' => $gateway['password'],
+ 'Transaction_Type' => '34',
+ 'Transaction_Tag' => $authorization->tran_id,
+ 'Authorization_Num' => $authorization->authorization_code,
+ 'Customer_Ref' => $charge['id'],
+ 'DollarAmount' => $charge['amount']
+ );
+
+ $trxnProperties = $this->CompleteArray($trxnProperties);
+
+ $post_response = $this->Process($trxnProperties, $post_url);
+
+ if ($post_response->EXact_Resp_Code == '00') {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ function CreateProfile($gateway, $customer, $credit_card, $subscription_id, $amount, $order_id)
+ {
+ $CI =& get_instance();
+
+ $post_url = $gateway['url_live'];
+
+ // Create the recurring seed
+
+ $transaction = array(
+ 'ExactID' => $gateway['terminal_id'],
+ 'Password' => $gateway['password'],
+ 'Transaction_Type' => '40',
+ 'Card_Number' => $credit_card['card_num'],
+ 'Expiry_Date' => str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . substr($credit_card['exp_year'],-2,2),
+ 'CVD_Presence_Ind' => '1',
+ 'Customer_Ref' => $order_id,
+ 'DollarAmount' => ((float)$amount == 0) ? '1' : $amount
+ );
+
+ if (isset($credit_card['cvv'])) {
+ $transaction['VerificationStr2'] = $credit_card['cvv'];
+ }
+
+ if (isset($customer['customer_id'])) {
+ $transaction['CardHoldersName'] = $customer['first_name'] . ' ' . $customer['last_name'];
+ } else {
+ $name = explode(' ', $credit_card['card_name']);
+ $transaction['CardHoldersName'] = $name[0] . ' ' . $name[1];
+ }
+
+ if (isset($customer['ip_address']) and !empty($customer['ip_address'])) {
+ $transaction['Client_IP'] = $customer['ip_address'];
+ }
+
+ $transaction = $this->CompleteArray($transaction);
+
+ $post_response = $this->Process($transaction, $post_url, $order_id);
+
+ if($post_response->EXact_Resp_Code == '00' and $post_response->Transaction_Approved === TRUE) {
+ $response['success'] = TRUE;
+ // Save the Auth information
+ $CI->load->model('billing/recurring_model');
+ $CI->recurring_model->SaveApiCustomerReference($subscription_id, $post_response->Transaction_Tag);
+ $CI->recurring_model->SaveApiAuthNumber($subscription_id, $post_response->Authorization_Num);
+ $response['transaction_tag'] = $post_response->Transaction_Tag;
+ $response['auth_num'] = $post_response->Authorization_Num;
+ } else {
+ $response['success'] = FALSE;
+ $response['reason'] = $post_response->EXact_Message;
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $order_id, $params['api_customer_reference'], $params['api_auth_number'], $params['amount']);
+ }
+
+ //--------------------------------------------------------------------
+
+ function ChargeRecurring($gateway, $order_id, $transaction_tag, $auth_num, $amount)
+ {
+ $CI =& get_instance();
+
+ $post_url = $gateway['url_live'];
+
+ // Create the charge
+
+ $trxnProperties = array(
+ 'ExactID' => $gateway['terminal_id'],
+ 'Password' => $gateway['password'],
+ 'Transaction_Type' => '30',
+ 'Transaction_Tag' => $transaction_tag,
+ 'Authorization_Num' => $auth_num,
+ 'Customer_Ref' => $order_id,
+ 'DollarAmount' => $amount
+ );
+
+ $trxnProperties = $this->CompleteArray($trxnProperties);
+
+ $post_response = $this->Process($trxnProperties, $post_url, $order_id);
+
+ $response = array();
+
+ if ($post_response->EXact_Resp_Code == '00' and $post_response->Transaction_Approved === TRUE) {
+ $response['success'] = TRUE;
+ // Save the Auth information
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $post_response->Transaction_Tag, $post_response->Authorization_Num);
+ } else {
+ $response['success'] = FALSE;
+ $response['reason'] = $post_response->EXact_Message;
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+ function UpdateRecurring()
+ {
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+ function Process($trxnProperties, $post_url)
+ {
+ $trxn = array("Transaction"=>$trxnProperties);
+//die(var_dump($trxn));
+ $client = new SoapClient($post_url);
+
+ $trxnResult = $client->__soapCall('SendAndCommit', $trxn);
+
+ return $trxnResult;
+ }
+
+ //--------------------------------------------------------------------
+
+ function CompleteArray($array = array())
+ {
+ $complete_if_blank = array(
+ "Ecommerce_Flag",
+ "XID",
+ "ExactID",
+ "CAVV",
+ "Password",
+ "CAVV_Algorithm",
+ "Transaction_Type",
+ "Reference_No",
+ "Customer_Ref",
+ "Reference_3",
+ "Client_IP",
+ "Client_Email",
+ "Language",
+ "Card_Number",
+ "Expiry_Date",
+ "CardHoldersName",
+ "Track1",
+ "Track2",
+ "Authorization_Num",
+ "Transaction_Tag",
+ "DollarAmount",
+ "VerificationStr1",
+ "VerificationStr2",
+ "CVD_Presence_Ind",
+ "Secure_AuthRequired",
+ "Secure_AuthResult",
+
+ // Level 2 fields
+ "ZipCode",
+ "Tax1Amount",
+ "Tax1Number",
+ "Tax2Amount",
+ "Tax2Number",
+
+ "SurchargeAmount", //Used for debit transactions only
+ "PAN",
+ "User_Name"
+ );
+
+ while (list(,$v) = each($complete_if_blank)) {
+ if (!key_exists($v, $array)) {
+ $array[$v] = '';
+ }
+ }
+
+ return $array;
+ }
+
+ //--------------------------------------------------------------------
+
+ public function get_url($gateway)
+ {
+ if (strpos($gateway['terminal_id'], 'AD') === 0)
+ {
+ // It needs a different endpoint.
+ //return 'https://api-demo.e-xact.com/transaction/wsdl';
+ }
+//return "https://secure2.e-xact.com/vplug-in/transaction/rpc-enc/service.asmx";
+ return $gateway['url_live'];
+ }
+
+ //--------------------------------------------------------------------
+
+
+ /*
+ Method: log_it()
+
+ Logs the transaction to a file. Helpful with debugging callback
+ transactions, since we can't actually see what's going on.
+
+ Parameters:
+ $heading - A string to be placed above the resutls
+ $params - Typically an array to print_r out so that we can inspect it.
+ */
+ public function log_it($heading, $params)
+ {
+ $file = FCPATH .'writeable/gateway_log.txt';
+
+ $content = '';
+ $content .= "# $heading\n";
+ $content .= date('Y-m-d H:i:s') ."\n\n";
+ $content .= print_r($params, true);
+ file_put_contents($file, $content, FILE_APPEND);
+ }
+
+ //--------------------------------------------------------------------
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/freshbooks.php b/app/modules/billing/libraries/payment/freshbooks.php
new file mode 100644
index 00000000..b70e3e6e
--- /dev/null
+++ b/app/modules/billing/libraries/payment/freshbooks.php
@@ -0,0 +1,436 @@
+settings = $this->Settings();
+ }
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'Online Invoicing with FreshBooks';
+ $settings['class_name'] = 'freshbooks';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = TRUE;
+ $settings['description'] = 'FreshBooks is a popular, versatile online invoicing application. Instead of collecting payments online, this gateway will create an invoice in your FreshBooks account for the customer. This way, you can accept payments offline (or via other methods) and track your income and accounts receivable with FreshBooks.';
+ $settings['is_preferred'] = 0;
+ $settings['setup_fee'] = 'n/a';
+ $settings['monthly_fee'] = 'n/a';
+ $settings['transaction_fee'] = 'n/a';
+ $settings['purchase_link'] = 'https://electricfunction.freshbooks.com/refer/www';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 1;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array(
+ 'enabled',
+ 'api_url',
+ 'auth_token',
+ 'item_name',
+ 'item_description'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'api_url' => array(
+ 'text' => 'API URL',
+ 'type' => 'text'
+ ),
+ 'auth_token' => array(
+ 'text' => 'Authentication Token',
+ 'type' => 'text'
+ ),
+ 'item_name' => array(
+ 'text' => 'Invoice Item Name',
+ 'type' => 'text'
+ ),
+ 'item_description' => array(
+ 'text' => 'Invoice Item Description',
+ 'type' => 'text'
+ )
+ );
+
+ return $settings;
+ }
+
+ function SendRequest ($api_url, $auth_token, $xml = '') {
+ $curl = curl_init($api_url);
+ curl_setopt($curl, CURLOPT_HEADER, 0);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_USERPWD, $auth_token);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $xml);
+ $result = curl_exec($curl);
+ curl_close($curl);
+
+ if (strpos($result,'load->library('array_to_xml');
+
+ $result = $CI->array_to_xml->toArray($result);
+
+ // sometimes this array is empty but it doesn't mean
+ // that the request didn't work
+ if (empty($result)) {
+ return TRUE;
+ }
+
+ return $result;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ function TestConnection ($gateway)
+ {
+ $xml = '
+
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (!empty($response)) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ function SaveClientID ($customer_id, $client_id) {
+ $CI =& get_instance();
+ $CI->load->model('billing/charge_data_model');
+ $CI->charge_data_model->Delete('fb_customer_' . $customer_id);
+ $CI->charge_data_model->Save('fb_customer_' . $customer_id, 'client_id', $client_id);
+ }
+
+ function GetCreateClient ($gateway, $customer) {
+ $CI =& get_instance();
+
+ // we may get arrays from GetCustomer which have the "id" key, not "customer_id"
+ $customer['customer_id'] = (isset($customer['id'])) ? $customer['id'] : $customer['customer_id'];
+
+ // does this client already exist in FreshBooks?
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get('fb_customer_' . $customer['customer_id']);
+
+ // if not, let's create it
+ if (empty($data) or !isset($data['client_id'])) {
+ // create the client
+ $company = (!empty($customer['company'])) ? $customer['company'] : $customer['first_name'] . ' ' . $customer['last_name'];
+ $phone = (!empty($customer['phone'])) ? '' . $customer['phone'] . ' ' : '';
+
+ if (!empty($customer['address_1'])) {
+ $address = '' . $customer['address_1'] . '
+ ' . $customer['address_2'] . '
+ ' . $customer['city'] . '
+ ' . $customer['state'] . '
+ ' . $customer['country'] . '
+ ' . $customer['postal_code'] . ' ';
+ }
+ else {
+ $address = '';
+ }
+
+ $xml = '
+
+
+ ' . $customer['first_name'] . '
+ ' . $customer['last_name'] . '
+ ' . $company . '
+ ' . $customer['email'] . '
+ ' . $phone . '
+ ' . $address .'
+
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (empty($response) or !isset($response['client_id'])) {
+ return FALSE;
+ }
+
+ $client_id = $response['client_id'];
+
+ // save this ID
+ $this->SaveClientID($customer['customer_id'], $client_id);
+
+ $created_client = TRUE;
+ }
+ else {
+ $client_id = $data['client_id'];
+ $created_client = FALSE;
+ }
+
+ // check to see if client is up to date...
+ if ($created_client == FALSE) {
+ $xml = '
+
+ ' . $client_id . '
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (empty($response) or !isset($response['client'])) {
+ // we have likely deleted the client, so let's create a new one
+ // create the client
+ $company = (!empty($customer['company'])) ? $customer['company'] : $customer['first_name'] . ' ' . $customer['last_name'];
+ $phone = (!empty($customer['phone'])) ? '' . $customer['phone'] . ' ' : '';
+
+ if (!empty($customer['address_1'])) {
+ $address = '' . $customer['address_1'] . '
+ ' . $customer['address_2'] . '
+ ' . $customer['city'] . '
+ ' . $customer['state'] . '
+ ' . $customer['country'] . '
+ ' . $customer['postal_code'] . ' ';
+ }
+ else {
+ $address = '';
+ }
+
+ $xml = '
+
+
+ ' . $customer['first_name'] . '
+ ' . $customer['last_name'] . '
+ ' . $company . '
+ ' . $customer['email'] . '
+ ' . $phone . '
+ ' . $address .'
+
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (empty($response) or !isset($response['client_id'])) {
+ return FALSE;
+ }
+
+ $client_id = $response['client_id'];
+
+ // save this ID
+ $this->SaveClientID($customer['customer_id'], $client_id);
+ }
+ else {
+ // compare to see if we need an update
+ $update_client = FALSE;
+ if ($response['client']['first_name'] != $customer['first_name']) {
+ $update_client = TRUE;
+ }
+ if ($response['client']['last_name'] != $customer['last_name']) {
+ $update_client = TRUE;
+ }
+ if ($response['client']['email'] != $customer['email']) {
+ $update_client = TRUE;
+ }
+ if ($response['client']['p_street1'] != $customer['address_1']) {
+ $update_client = TRUE;
+ }
+ if ($response['client']['p_city'] != $customer['city']) {
+ $update_client = TRUE;
+ }
+ if ($response['client']['p_code'] != $customer['postal_code']) {
+ $update_client = TRUE;
+ }
+
+ if ($update_client == TRUE) {
+ // update the client
+ $company = (!empty($customer['company'])) ? $customer['company'] : $customer['first_name'] . ' ' . $customer['last_name'];
+ $phone = (!empty($customer['phone'])) ? '' . $customer['phone'] . ' ' : '';
+
+ if (!empty($customer['address_1'])) {
+ $address = '' . $customer['address_1'] . '
+ ' . $customer['address_2'] . '
+ ' . $customer['city'] . '
+ ' . $customer['state'] . '
+ ' . $customer['country'] . '
+ ' . $customer['postal_code'] . ' ';
+ }
+ else {
+ $address = '';
+ }
+
+ $xml = '
+
+
+ ' . $client_id . '
+ ' . $customer['first_name'] . '
+ ' . $customer['last_name'] . '
+ ' . $company . '
+ ' . $customer['email'] . '
+ ' . $phone . '
+ ' . $address .'
+
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (empty($response)) {
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ // and return the client ID...
+ return $client_id;
+ }
+
+ function Charge ($order_id, $gateway, $customer, $amount, $credit_card, $return_url, $cancel_url)
+ {
+ $CI =& get_instance();
+
+ $fb_client_id = $this->GetCreateClient($gateway, $customer);
+ if (empty($fb_client_id)) {
+ return $CI->response->TransactionResponse(2, array('reason' => 'Unable to create or retrieve the client ID properly.'));
+ }
+
+ // create the invoice
+ $xml = '
+
+ ' . $fb_client_id . '
+ sent
+ ' . $return_url . '
+
+
+
+ ' . $gateway['item_name'] . '
+ ' . $gateway['item_description'] . ' (Charge #' . $order_id . ')
+ ' . $amount . '
+ 1
+
+
+
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (empty($response) or !isset($response['invoice_id'])) {
+ return $CI->response->TransactionResponse(2, array('reason' => 'Unable to create invoice.'));
+ }
+
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+
+ return $response;
+ }
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE, $return_url = '', $cancel_url = '')
+ {
+ $CI =& get_instance();
+
+ // if a payment is to be made today, process it.
+ if ($charge_today === TRUE) {
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ $response = $this->Charge($order_id, $gateway, $customer, $amount, array(), $return_url, $cancel_url);
+
+ if (!empty($response) and $response['response_code'] != '2') {
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ }
+ else {
+ $response = $CI->response->TransactionResponse(2, array('reason' => 'Unable to create initial invoice.'));
+ }
+ }
+ else {
+ // we'll create the FreshBooks client, but that's it
+ $fb_client_id = $this->GetCreateClient($gateway, $customer);
+
+ if (!empty($fb_client_id)) {
+ $response = $CI->response->TransactionResponse(100, array('recurring_id' => $subscription_id));
+ }
+ else {
+ $response = $CI->response->TransactionResponse(2, array('reason' => 'Unable to create the FreshBooks client.'));
+ }
+ }
+
+ return $response;
+ }
+
+ function Refund ($gateway, $charge, $authorization)
+ {
+ $xml = '
+
+ 344
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (!empty($response)) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ $CI =& get_instance();
+
+ // get customer array
+ $CI->load->model('billing/customer_model');
+ $customer = $CI->customer_model->GetCustomer($params['customer_id']);
+
+ $fb_client_id = $this->GetCreateClient($gateway, $customer);
+ if (empty($fb_client_id)) {
+ return array('success' => FALSE, 'reason' => 'Unable to retrieve FreshBooks client ID.');
+ }
+
+ // create the invoice
+ $xml = '
+
+ ' . $fb_client_id . '
+ sent
+
+
+
+ ' . $gateway['item_name'] . '
+ ' . $gateway['item_description'] . ' (Recurring #' . $params['subscription_id'] . ' & Charge #' . $order_id . ')
+ ' . $params['amount'] . '
+ 1
+
+
+
+ ';
+
+ $response = $this->SendRequest($gateway['api_url'], $gateway['auth_token'], $xml);
+
+ if (empty($response) or !isset($response['invoice_id'])) {
+ return array('success' => FALSE, 'reason' => 'Unable to create recurring FreshBooks invoice.');
+ }
+ else {
+ $response = array();
+ $response['success'] = TRUE;
+
+ return $response;
+ }
+ }
+
+ function UpdateRecurring()
+ {
+ return TRUE;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/offline.php b/app/modules/billing/libraries/payment/offline.php
new file mode 100644
index 00000000..830efcac
--- /dev/null
+++ b/app/modules/billing/libraries/payment/offline.php
@@ -0,0 +1,102 @@
+settings = $this->Settings();
+ }
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'Offline, Cheque, & Money Order';
+ $settings['class_name'] = 'offline';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = TRUE;
+ $settings['description'] = 'Record offline payments with this gateway. One-time charges are simply recorded in the system. Subscription payments are assumed paid until the subscription is cancelled or expires.';
+ $settings['is_preferred'] = 0;
+ $settings['setup_fee'] = 'n/a';
+ $settings['monthly_fee'] = 'n/a';
+ $settings['transaction_fee'] = 'n/a';
+ $settings['purchase_link'] = '';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 0;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array(
+ 'enabled'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ )
+ );
+
+ return $settings;
+ }
+
+ function TestConnection ($gateway)
+ {
+ return TRUE;
+ }
+
+ function Charge ($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+
+ return $response;
+ }
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE)
+ {
+ $CI =& get_instance();
+ // if a payment is to be made today, process it.
+ if ($charge_today === TRUE) {
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ }
+ else {
+ $response = $CI->response->TransactionResponse(100, array('recurring_id' => $subscription_id));
+ }
+
+ return $response;
+ }
+
+ function Refund ($gateway, $charge, $authorization)
+ {
+ return TRUE;
+ }
+
+ function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ $response = array();
+ $response['success'] = TRUE;
+
+ return $response;
+ }
+
+ function UpdateRecurring()
+ {
+ return TRUE;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/pacnet.php b/app/modules/billing/libraries/payment/pacnet.php
new file mode 100644
index 00000000..f13cc462
--- /dev/null
+++ b/app/modules/billing/libraries/payment/pacnet.php
@@ -0,0 +1,393 @@
+settings = $this->Settings();
+ }
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'Pacnet';
+ $settings['class_name'] = 'pacnet';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'Pacnet, and its RAVEN payment system, are a great way for companies to process transactions online.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = 'n/a';
+ $settings['monthly_fee'] = 'n/a';
+ $settings['transaction_fee'] = 'n/a';
+ $settings['purchase_link'] = 'http://www.pacnetservices.com';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 0;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array('enabled',
+ 'username',
+ 'password',
+ 'prn',
+ 'currency',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_dc',
+ 'accept_amex');
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'username' => array(
+ 'text' => 'Raven Username',
+ 'type' => 'text'
+ ),
+ 'password' => array(
+ 'text' => 'Raven Shared Secret Password',
+ 'type' => 'password'
+ ),
+ 'prn' => array(
+ 'text' => 'Payment Routing Number',
+ 'type' => 'text'
+ ),
+ 'currency' => array(
+ 'text' => 'Currency',
+ 'type' => 'select',
+ 'options' => array(
+ 'GBP' => 'GBP - Pound Sterling',
+ 'EUR' => 'EUR - Euro',
+ 'USD' => 'USD - US Dollar',
+ 'AUD' => 'AUD - Australian Dollar',
+ 'CAD' => 'CAD - Canadian Dollar',
+ 'CHF' => 'CHF - Swiss Franc',
+ 'DKK' => 'DKK - Danish Krone',
+ 'HKD' => 'HKD - Hong Kong Dollar',
+ 'IDR' => 'IDR - Rupiah',
+ 'JPY' => 'JPY - Yen',
+ 'LUF' => 'LUF - Luxembourg Franc',
+ 'NOK' => 'NOK - Norwegian Krone',
+ 'NZD' => 'NZD - New Zealand Dollar',
+ 'SEK' => 'SEK - Swedish Krona',
+ 'SGD' => 'SGD - Singapore Dollar',
+ 'TRL' => 'TRL - Turkish Lira'
+ )
+ ),
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_dc' => array(
+ 'text' => 'Accept Diner\'s Club?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ function TestConnection($gateway)
+ {
+ $response = $this->Process($gateway, array('UserName','Timestamp'),array(),'hello');
+
+ if (strstr($response['Response'],'Hello')) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ $variables = array(
+ 'PaymentRoutingNumber' => $gateway['prn'],
+ 'PaymentType' => 'cc_debit',
+ 'Amount' => (int)($amount * 100),
+ 'CurrencyCode' => $gateway['currency'],
+ 'CardNumber' => $credit_card['card_num'],
+ 'ExpiryDate' => str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . substr($credit_card['exp_year'],-2,2),
+ 'Reference' => $order_id
+ );
+
+ if (isset($credit_card['cvv'])) {
+ $variables['CVV2'] = $credit_card['cvv'];
+ }
+
+ $variables['CardIssuerName'] = $credit_card['name'];
+
+ if (isset($customer['ip_address']) and !empty($customer['ip_address'])) {
+ $variables['CustomerIP'] = $customer['ip_address'];
+ }
+
+ $response = $this->Process($gateway, array('UserName','Timestamp','Amount','CurrencyCode','Reference'), $variables, 'submit');
+
+ if (isset($response['Status']) and ($response['Status'] == 'Approved' or $response['Status'] == 'Submitted' or $response['Status'] == 'InProgress')) {
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['TrackingNumber'], '');
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ } else {
+ $response_array = array('reason' => (isset($response['Status'])) ? 'Error ' . $response['Status'] : 'Undefined Error');
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE)
+ {
+ $CI =& get_instance();
+
+ $CI->load->model('billing/order_authorization_model');
+
+ // is there a payment for today?
+ if ($charge_today === TRUE) {
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $customer['customer_id'] = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ $variables = array(
+ 'PaymentRoutingNumber' => $gateway['prn'],
+ 'PaymentType' => 'cc_debit',
+ 'Amount' => (int)($amount * 100),
+ 'CurrencyCode' => $gateway['currency'],
+ 'CardNumber' => $credit_card['card_num'],
+ 'ExpiryDate' => str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . substr($credit_card['exp_year'],-2,2),
+ 'Reference' => $order_id
+ );
+
+ if (isset($credit_card['cvv'])) {
+ $variables['CVV2'] = $credit_card['cvv'];
+ }
+
+ $variables['CardIssuerName'] = $credit_card['name'];
+
+ if (isset($customer['ip_address']) and !empty($customer['ip_address'])) {
+ $variables['CustomerIP'] = $customer['ip_address'];
+ }
+
+ $response = $this->Process($gateway, array('UserName','Timestamp','Amount','CurrencyCode','Reference'), $variables, 'submit');
+
+ if (isset($response['Status']) and ($response['Status'] == 'Approved' or $response['Status'] == 'Submitted' or $response['Status'] == 'InProgress')) {
+ $CI->recurring_model->SaveApiAuthNumber($subscription_id, $response['TrackingNumber']);
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['TrackingNumber'], '');
+
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response_array = array('reason' => (isset($response['Status'])) ? 'Error ' . $response['Status'] : 'Undefined Error');
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ } else {
+ // authorize
+ $variables = array(
+ 'PaymentRoutingNumber' => $gateway['prn'],
+ 'PaymentType' => 'cc_preauth',
+ 'Amount' => '1',
+ 'CurrencyCode' => $gateway['currency'],
+ 'CardNumber' => $credit_card['card_num'],
+ 'ExpiryDate' => str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . substr($credit_card['exp_year'],-2,2),
+ 'Reference' => md5(time())
+ );
+
+ if (isset($credit_card['cvv'])) {
+ $variables['CVV2'] = $credit_card['cvv'];
+ }
+
+ $variables['CardIssuerName'] = $credit_card['name'];
+
+ if (isset($customer['ip_address']) and !empty($customer['ip_address'])) {
+ $variables['CustomerIP'] = $customer['ip_address'];
+ }
+
+ $response = $this->Process($gateway, array('UserName','Timestamp','Amount','CurrencyCode','Reference'), $variables, 'submit');
+
+ if (isset($response['Status']) and ($response['Status'] == 'Approved' or $response['Status'] == 'Submitted' or $response['Status'] == 'InProgress')) {
+ $CI->recurring_model->SaveApiAuthNumber($subscription_id, $response['TrackingNumber']);
+
+ $response_array = array('recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+
+ $response_array = array('reason' => (isset($response['Status'])) ? 'Error ' . $response['Status'] : 'Undefined Error');
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ }
+
+ return $response;
+ }
+
+ function Refund ($gateway, $charge, $authorization)
+ {
+ $CI =& get_instance();
+
+ $variables = array(
+ 'PaymentRoutingNumber' => $gateway['prn'],
+ 'PaymentType' => 'cc_credit',
+ 'Amount' => (int)($charge['amount'] * 100),
+ 'CurrencyCode' => $gateway['currency'],
+ 'TemplateNumber' => $authorization->tran_id,
+ 'Reference' => md5(time())
+ );
+
+ $response = $this->Process($gateway, array('UserName','Timestamp','Amount','CurrencyCode','Reference'), $variables, 'submit');
+
+ if (isset($response['Status']) and ($response['Status'] == 'Approved' or $response['Status'] == 'Submitted' or $response['Status'] == 'InProgress')) {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $order_id, $params['api_auth_number'], $params['amount']);
+ }
+
+ function ChargeRecurring($gateway, $order_id, $template_number, $amount)
+ {
+ $CI =& get_instance();
+
+ $variables = array(
+ 'PaymentRoutingNumber' => $gateway['prn'],
+ 'PaymentType' => 'cc_debit',
+ 'Amount' => (int)($amount * 100),
+ 'CurrencyCode' => $gateway['currency'],
+ 'TemplateNumber' => $template_number,
+ 'Reference' => md5(time())
+ );
+
+ $response = $this->Process($gateway, array('UserName','Timestamp','Amount','CurrencyCode','Reference'), $variables, 'submit');
+
+ if (isset($response['Status']) and ($response['Status'] == 'Approved' or $response['Status'] == 'Submitted' or $response['Status'] == 'InProgress')) {
+ $response['success'] = TRUE;
+ // Save the Auth information
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['TrackingNumber']);
+ } else {
+ $response['success'] = FALSE;
+ $response['reason'] = (isset($response['Status'])) ? 'Error ' . $response['Status'] : 'Undefined Error';
+ }
+
+ return $response;
+ }
+
+ function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ function UpdateRecurring()
+ {
+ return TRUE;
+ }
+
+ function Process($gateway, $signature_variables = array(), $variables = array(), $method = 'hello')
+ {
+ $url = $this->GetAPIURL($gateway) . '/' . $method;
+
+ // start post_string
+ $post_string = '';
+ foreach ($variables as $key => $value) {
+ $post_string .= $key . '=' . urlencode($value) . '&';
+ }
+
+ // build signature
+ $variables['UserName'] = $gateway['username'];
+ $variables['Timestamp'] = gmdate('Y-m-d\TH:i:s.000\Z');
+
+ $signature_string = '';
+ foreach ($signature_variables as $variable) {
+ $signature_string .= $variables[$variable] . ',';
+ }
+ $signature_string = rtrim($signature_string, ',');
+
+ $sha1 = strtoupper(sha1($signature_string));
+
+ $signature = strtoupper(sha1($sha1 . ',' . $gateway['password']));
+
+ // finish $post_string
+ $post_string .= 'UserName=' . urlencode($variables['UserName']) . '&Timestamp=' . urlencode($variables['Timestamp']) . '&Signature=' . $signature;
+
+ $request = curl_init($url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_POSTFIELDS, $post_string); // use HTTP POST to send form data
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+ curl_close ($request); // close curl object
+
+ $response = array();
+
+ $pairs = explode('&', $post_response);
+
+ if (is_array($pairs)) {
+ foreach ($pairs as $pair)
+ {
+ list($key, $value) = explode('=', $pair);
+ if ($key != '')
+ {
+ $key = htmlspecialchars(urldecode($key));
+ $value = htmlspecialchars(urldecode($value));
+ $response[$key] = $value;
+ }
+ }
+ }
+ else {
+ return FALSE;
+ }
+
+ return $response;
+ }
+
+ function GetAPIURL ($gateway) {
+ return $gateway['url_live'];
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/paypal.php b/app/modules/billing/libraries/payment/paypal.php
new file mode 100644
index 00000000..e6418bd0
--- /dev/null
+++ b/app/modules/billing/libraries/payment/paypal.php
@@ -0,0 +1,707 @@
+settings = $this->Settings();
+ }
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'PayPal Pro';
+ $settings['class_name'] = 'paypal';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'PayPal Pro is easy to setup and even easier to use. Though not as powerful as other gateways (you cannot edit existing subscriptions, only cancel them), this gateway is very easy to setup. Requires the Recurring Billing addon.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$0.00';
+ $settings['monthly_fee'] = '$30.00';
+ $settings['transaction_fee'] = '2.5% + $0.30';
+ $settings['purchase_link'] = 'https://www.paypal.com/ca/mrb/pal=Q4XUN8HMLDQ2N';
+ $settings['allows_updates'] = 0;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 1;
+ $settings['requires_customer_ip'] = 1;
+ $settings['required_fields'] = array('enabled',
+ 'mode',
+ 'user',
+ 'pwd',
+ 'signature',
+ 'currency',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_dc',
+ 'accept_amex'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'test' => 'Sandbox'
+ )
+ ),
+ 'user' => array(
+ 'text' => 'API Username',
+ 'type' => 'text'
+ ),
+ 'pwd' => array(
+ 'text' => 'API Password',
+ 'type' => 'password'
+ ),
+ 'signature' => array(
+ 'text' => 'API Signature',
+ 'type' => 'text',
+ ),
+ 'currency' => array(
+ 'text' => 'Currency',
+ 'type' => 'select',
+ 'options' => array(
+ 'USD' => 'US Dollar',
+ 'AUD' => 'Australian Dollar',
+ 'CAD' => 'Canadian Dollar',
+ 'EUR' => 'Euro',
+ 'GBP' => 'British Pound',
+ 'JPY' => 'Japanese Yen',
+ 'NZD' => 'New Zealand Dollar',
+ 'CHF' => 'Swiss Franc',
+ 'HKD' => 'Hong Kong Dollar',
+ 'SGD' => 'Singapore Dollar',
+ 'SEK' => 'Swedish Krona',
+ 'DKK' => 'Danish Krone',
+ 'PLN' => 'Polish Zloty',
+ 'NOK' => 'Norwegian Krone',
+ 'HUF' => 'Hungarian Forint',
+ 'CZK' => 'Czech Koruna',
+ 'ILS' => 'Israeli New Shekel',
+ 'MXN' => 'Mexican Peso',
+ 'BRL' => 'Brazilian Real',
+ 'MYR' => 'Malaysian Ringgit',
+ 'PHP' => 'Philippine Peso',
+ 'TWD' => 'New Taiwan Dollar',
+ 'THB' => 'Thai Baht',
+ 'TRY' => 'Turkish Lira'
+ )
+ ),
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_dc' => array(
+ 'text' => 'Accept Diner\'s Club?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+ return $settings;
+ }
+
+ function TestConnection($gateway)
+ {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ }
+
+ $post = array();
+ $post['version'] = '56.0';
+ $post['method'] = 'GetBalance';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+
+ $response = $this->Process($post_url, $post);
+
+ $response = $this->response_to_array($response);
+
+ $CI =& get_instance();
+
+ if($response['ACK'] == 'Success') {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ // get card type in proper format
+ switch($credit_card['card_type']) {
+ case 'visa';
+ $card_type = 'Visa';
+ break;
+ case 'mc';
+ $card_type = 'MasterCard';
+ break;
+ case 'disc';
+ $card_type = 'Discover';
+ break;
+ case 'amex';
+ $card_type = 'Amex';
+ break;
+ }
+
+ // Get the proper URL
+ switch($gateway['mode']) {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ }
+
+ // prep exp_date
+ if (strlen($credit_card['exp_year']) == 2) {
+ $credit_card['exp_year'] = '20' . $credit_card['exp_year'];
+ }
+
+ $post = array();
+ $post['version'] = '56.0';
+ $post['paymentaction'] = 'sale';
+ $post['method'] = 'DoDirectPayment';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['amt'] = $amount;
+ $post['acct'] = $credit_card['card_num'];
+ $post['creditcardtype'] = $card_type;
+ $post['expdate'] = str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . $credit_card['exp_year'];
+ $post['invnum'] = $order_id;
+ $post['currencycode'] = $gateway['currency'];
+ $post['ipaddress'] = $customer['ip_address'];
+
+ if (isset($credit_card['cvv'])) {
+ $post['cvv2'] = $credit_card['cvv'];
+ }
+
+ if (isset($customer['customer_id'])) {
+ $post['firstname'] = $customer['first_name'];
+ $post['lastname'] = $customer['last_name'];
+ $post['street'] = $customer['address_1'].$customer['address_2'];
+ $post['city'] = $customer['city'];
+ $post['state'] = $customer['state'];
+ $post['zip'] = $customer['postal_code'];
+ $post['countrycode'] = $customer['country'];
+ $post['phonenum'] = $customer['phone'];
+ $post['email'] = $customer['email'];
+ }
+
+ $response = $this->Process($post_url, $post, $order_id);
+
+ $response = $this->response_to_array($response);
+
+ if ($response['ACK'] == 'Success' or $response['ACK'] == 'SuccessWithWarning') {
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['TRANSACTIONID']);
+
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ }
+ else {
+ $response_array = array('reason' => $response['L_ERRORCODE0'] . ' - ' . $response['L_LONGMESSAGE0']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ function Recur($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences)
+ {
+ $CI =& get_instance();
+
+ // prep exp_date
+ if (strlen($credit_card['exp_year']) == 2) {
+ $credit_card['exp_year'] = '20' . $credit_card['exp_year'];
+ }
+
+ // If the start date is today, we'll do the first one manually
+ if ($charge_today === TRUE) {
+ // Create an order
+ $CI->load->model('billing/charge_model');
+
+ $customer['customer_id'] = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+ $response = $this->Charge($order_id, $gateway, $customer, $amount, $credit_card);
+
+ if ($response['response_code'] == 1) {
+ $response_array['charge_id'] = $response['charge_id'];
+ $start_date = date('Y-m-d', strtotime($start_date) + ($interval * 86400));
+ $CI->charge_model->SetStatus($order_id, 1);
+ } else {
+ $CI->load->model('billing/recurring_model');
+ $CI->recurring_model->MakeInactive($subscription_id);
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+
+ return $response;
+ }
+ }
+
+ // we need to create a success variable
+ $profile_success = FALSE;
+
+ // if we only have 1 charge, we don't need a recurring profile
+ if ($charge_today != TRUE or $total_occurrences != 1) {
+ // setup recurring profile
+
+ // get true recurring rate, first
+ $subscription = $CI->recurring_model->GetRecurring($subscription_id);
+
+ // Create a new PayPal profile
+ $response = $this->CreateProfile($gateway, $customer, $subscription['amount'], $credit_card, $start_date, $subscription_id, $total_occurrences, $interval);
+
+ if (is_array($response) and $response['success'] == TRUE) {
+ $profile_id = $response['profile_id'];
+
+ $CI->recurring_model->SaveApiCustomerReference($subscription_id, $profile_id);
+
+ $profile_success = TRUE;
+ }
+ }
+ else {
+ $profile_success = TRUE;
+ }
+
+ if ($profile_success == TRUE){
+ $response_array['recurring_id'] = $subscription_id;
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ function Refund ($gateway, $charge, $authorization)
+ {
+ $CI =& get_instance();
+
+ // Get the proper URL
+ switch($gateway['mode']) {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ }
+
+ $post = array();
+ $post['version'] = '56.0';
+ $post['method'] = 'RefundTransaction';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['transactionid'] = $authorization->tran_id;
+ $post['refundtype'] = 'FULL';
+
+ $response = $this->Process($post_url, $post);
+ $response = $this->response_to_array($response);
+
+ if ($response['ACK'] == 'Success') {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ function Process($url, $post_data, $order_id = FALSE)
+ {
+ $CI =& get_instance();
+
+ $data = '';
+
+ // Build the data string for the request body
+ foreach($post_data as $key => $value)
+ {
+ if(!empty($value))
+ {
+ $data .= strtoupper($key) . '=' . urlencode($value) . '&';
+ }
+ }
+
+ // remove the extra ampersand
+ $data = substr($data, 0, strlen($data) - 1);
+
+ // setting the curl parameters.
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_VERBOSE, 1);
+
+ // turning off the server and peer verification(TrustManager Concept).
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
+
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_POST, 1);
+
+ // setting the nvpreq as POST FIELD to curl
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+
+ // getting response from server
+ $response = curl_exec($ch);
+
+ return $response;
+
+ }
+
+ function CreateProfile($gateway, $customer, $amount, $credit_card, $start_date, $subscription_id, $total_occurrences, $interval)
+ {
+ $CI =& get_instance();
+
+ switch($credit_card['card_type']) {
+ case 'visa';
+ $card_type = 'Visa';
+ break;
+ case 'mc';
+ $card_type = 'MasterCard';
+ break;
+ case 'disc';
+ $card_type = 'Discover';
+ break;
+ case 'amex';
+ $card_type = 'Amex';
+ break;
+ }
+
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['arb_url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['arb_url_test'];
+ break;
+ }
+
+ $post = array();
+ $post['version'] = '60';
+ $post['method'] = 'CreateRecurringPaymentsProfile';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['amt'] = $amount;
+ $post['acct'] = $credit_card['card_num'];
+ $post['currencycode'] = $gateway['currency'];
+ $post['creditcardtype'] = $card_type;
+ $post['expdate'] = str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . $credit_card['exp_year'];
+ $post['billingperiod'] = 'Day';
+ $post['billingfrequency'] = $interval;
+ $post['profilestartdate'] = date('c', strtotime($start_date));
+ $post['ipaddress'] = (isset($customer['ip_address'])) ? $customer['ip_address'] : '0.0.0.0';
+
+ if(isset($credit_card['cvv'])) {
+ $post['cvv2'] = $credit_card['cvv'];
+ }
+
+ // build customer address
+ $post['firstname'] = (isset($customer['first_name'])) ? $customer['first_name'] : '';
+ $post['lastname'] = (isset($customer['last_name'])) ? $customer['last_name'] : '';
+ $post['street'] = (isset($customer['address_1'])) ? $customer['address_1'] : '';
+ if (isset($customer['address_2'])) {
+ $post['street'] .= ' ' . $customer['address_2'];
+ }
+ $post['city'] = (isset($customer['city'])) ? $customer['city'] : '';
+ $post['state'] = (isset($customer['state'])) ? $customer['state'] : '';
+ $post['countrycode'] = (isset($customer['country'])) ? $customer['country'] : '';
+ $post['zip'] = (isset($customer['postal_code'])) ? $customer['postal_code'] : '';
+ $post['email'] = (isset($customer['email'])) ? $customer['email'] : '';
+
+ // Get the company name
+ $company = setting('site_name');
+ $post['desc'] = $company.' Subscription';
+
+ $post_response = $this->Process($post_url, $post);
+
+ $post_response = $this->response_to_array($post_response);
+
+ if($post_response['ACK'] == 'Success') {
+ $response['success'] = TRUE;
+ $response['profile_id'] = $post_response['PROFILEID'];
+ } else {
+ $response['success'] = FALSE;
+ $response['profile_id'] = FALSE;
+ $response['reason'] = $post_response['L_LONGMESSAGE0'];
+ }
+
+ return $response;
+ }
+
+ function CancelRecurring($subscription, $gateway)
+ {
+ // was this a one time charge? if so, we didn't create a recurring profile...
+ if (empty($subscription['api_customer_reference'])) {
+ return TRUE;
+ }
+
+ $CI =& get_instance();
+ $CI->load->model('billing/recurring_model');
+
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $subscription['arb_prod_url'];
+ break;
+ case 'test':
+ $post_url = $subscription['arb_test_url'];
+ break;
+ case 'dev':
+ $post_url = $subscription['arb_dev_url'];
+ break;
+ }
+
+ $post = array();
+ $post['version'] = '60';
+ $post['method'] = 'ManageRecurringPaymentsProfileStatus';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['profileid'] = $subscription['api_customer_reference'];
+ $post['action'] = 'Cancel';
+
+ $post_response = $this->Process($post_url, $post);
+
+ $post_response = $this->response_to_array($post_response);
+
+ if($post_response['ACK'] == 'Success') {
+ $response = TRUE;
+ } else {
+ $response = FALSE;
+ }
+
+ return $response;
+ }
+
+ function UpdateRecurring($gateway, $subscription, $customer, $params)
+ {
+ // was this a one time charge? if so, we didn't create a recurring profile...
+ if (empty($subscription['api_customer_reference'])) {
+ return FALSE;
+ }
+
+ $CI =& get_instance();
+ $CI->load->model('billing/recurring_model');
+
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $subscription['arb_prod_url'];
+ break;
+ case 'test':
+ $post_url = $subscription['arb_test_url'];
+ break;
+ case 'dev':
+ $post_url = $subscription['arb_dev_url'];
+ break;
+ }
+
+ $post = array();
+ $post['version'] = '58.0';
+ $post['method'] = 'UpdateRecurringPaymentsProfile';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['profileid'] = $subscription['api_customer_reference'];
+
+ if(isset($params['amount'])) {
+ $post['currencycode'] = $gateway['currency'];
+ $post['amt'] = $params['amount'];
+ }
+
+ if(isset($params['customer_id'])){
+
+ $post['firstname'] = $customer['first_name'];
+ $post['lastname'] = $customer['last_name'];
+ $post['street'] = $customer['address_1'];
+
+ if($customer['address_1'] != '') {
+ $post['street'] .= ' '.$customer['address_2'];
+ }
+
+ $post['city'] = $customer['city'];
+ $post['state'] = $customer['state'];
+ $post['zip'] = $customer['postal_code'];
+ }
+
+ $post_response = $this->Process($post_url, $post);
+
+ $post_response = $this->response_to_array($post_response);
+
+ if($post_response['ACK'] == 'Success') {
+ $response = TRUE;
+ } else {
+ $response = FALSE;
+ }
+
+ return $response;
+ }
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $params);
+ }
+
+ function ChargeRecurring($gateway, $params)
+ {
+ $CI =& get_instance();
+ $CI->load->library('billing/transaction_log');
+ $CI->load->model('billing/recurring_model');
+
+ $details = $this->GetProfileDetails($gateway, $params);
+
+ $CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'paypal_profile_details', $details, __FILE__, __LINE__);
+
+ if (!$details) {
+ // if we didn't retrieve the profile properly, we'd rather let the subscription
+ // go then cancel it due to a one-time connection issue
+ return array('success' => TRUE);
+ }
+
+ /*
+ * We used to check for failed payments but PayPal only marks them failed after like
+ 15 days...
+ */
+ $failed_payments = $details['FAILEDPAYMENTCOUNT'];
+ $status = $details['STATUS'];
+
+ $response = array();
+
+ if ($status != 'Cancelled' and (int)$failed_payments === 0) {
+ $response['success'] = TRUE;
+
+ // should we cancel this subscription? i.e., will it expire before the next renew?
+ // this is only important because PayPal's charge scheduling sometimes jumps the gun
+ if (strtotime($params['end_date']) <= (strtotime($params['next_charge']) + (60*60*24*$params['charge_interval']))) {
+ // silently cancel the subscription
+ $CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'paypal_pre_cancel', FALSE, __FILE__, __LINE__);
+ $next_charge = $CI->recurring_model->GetNextChargeDate($params['subscription_id'], $params['next_charge']);
+ $CI->db->update('subscriptions', array('next_charge' => $next_charge), array('subscription_id' => $params['subscription_id']));
+ $CI->recurring_model->CancelRecurring($params['subscription_id'], TRUE);
+ }
+ } else {
+ if ((int)$failed_payments > 0) {
+ $CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'paypal_charge_recurring_response', array('response' => 'Failed payments: ' . $failed_payments), __FILE__, __LINE__);
+ }
+ else {
+ $CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'paypal_charge_recurring_response', array('response' => 'Status: ' . $status), __FILE__, __LINE__);
+ }
+
+ $response['success'] = FALSE;
+ $response['reason'] = "The charge has failed.";
+ }
+
+ return $response;
+ }
+
+ function GetProfileDetails($gateway, $params)
+ {
+ $CI =& get_instance();
+ $CI->load->model('billing/recurring_model');
+
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['arb_url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['arb_url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['arb_url_dev'];
+ break;
+ default:
+ $post_url = $gateway['arb_url_dev'];
+ break;
+ }
+
+ $post = array();
+ $post['version'] = '60';
+ $post['method'] = 'GetRecurringPaymentsProfileDetails';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['profileid'] = $params['api_customer_reference'];
+
+ $post_response = $this->Process($post_url, $post);
+ $response = $this->response_to_array($post_response);
+
+ if ($response['ACK'] == 'Success') {
+ return $response;
+ } else {
+ return FALSE;
+ }
+ }
+
+ private function response_to_array($string)
+ {
+ $string = urldecode($string);
+ $pairs = explode('&', $string);
+ $values = array();
+
+ foreach($pairs as $pair)
+ {
+ list($key, $value) = explode('=', $pair);
+ $values[$key] = $value;
+ }
+
+ return $values;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/paypal_standard.php b/app/modules/billing/libraries/payment/paypal_standard.php
new file mode 100644
index 00000000..1bd4e26f
--- /dev/null
+++ b/app/modules/billing/libraries/payment/paypal_standard.php
@@ -0,0 +1,958 @@
+settings = $this->Settings();
+ }
+
+ //--------------------------------------------------------------------
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'PayPal Express Checkout';
+ $settings['class_name'] = 'paypal_standard';
+ $settings['external'] = TRUE;
+ $settings['no_credit_card'] = TRUE;
+ $settings['description'] = 'PayPal Express Checkout is the easiest, cheapest way to accept payments online. Any Website Payments Standard account supports it.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$0';
+ $settings['monthly_fee'] = '$0';
+ $settings['transaction_fee'] = '2.9% + $0.30';
+ $settings['purchase_link'] = 'https://www.paypal.com/ca/mrb/pal=Q4XUN8HMLDQ2N';
+ $settings['allows_updates'] = 0;
+ $settings['allows_refunds'] = 0;
+ $settings['requires_customer_information'] = 0;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array('enabled',
+ 'mode',
+ 'user',
+ 'pwd',
+ 'signature',
+ 'currency'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'test' => 'Sandbox'
+ )
+ ),
+ 'user' => array(
+ 'text' => 'API Username',
+ 'type' => 'text'
+ ),
+ 'pwd' => array(
+ 'text' => 'API Password',
+ 'type' => 'password'
+ ),
+ 'signature' => array(
+ 'text' => 'API Signature',
+ 'type' => 'text',
+ ),
+ 'currency' => array(
+ 'text' => 'Currency',
+ 'type' => 'select',
+ 'options' => array(
+ 'USD' => 'US Dollar',
+ 'AUD' => 'Australian Dollar',
+ 'CAD' => 'Canadian Dollar',
+ 'EUR' => 'Euro',
+ 'GBP' => 'British Pound',
+ 'JPY' => 'Japanese Yen',
+ 'NZD' => 'New Zealand Dollar',
+ 'CHF' => 'Swiss Franc',
+ 'HKD' => 'Hong Kong Dollar',
+ 'SGD' => 'Singapore Dollar',
+ 'SEK' => 'Swedish Krona',
+ 'DKK' => 'Danish Krone',
+ 'PLN' => 'Polish Zloty',
+ 'NOK' => 'Norwegian Krone',
+ 'HUF' => 'Hungarian Forint',
+ 'CZK' => 'Czech Koruna',
+ 'ILS' => 'Israeli New Shekel',
+ 'MXN' => 'Mexican Peso',
+ 'BRL' => 'Brazilian Real',
+ 'MYR' => 'Malaysian Ringgit',
+ 'PHP' => 'Philippine Peso',
+ 'TWD' => 'New Taiwan Dollar',
+ 'THB' => 'Thai Baht',
+ 'TRY' => 'Turkish Lira'
+ )
+ )
+ );
+ return $settings;
+ }
+
+ //--------------------------------------------------------------------
+
+ function TestConnection($gateway)
+ {
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card, $return_url, $cancel_url)
+ {
+ $CI =& get_instance();
+ $CI->load->model('billing/charge_data_model');
+ $CI->load->helper('url');
+
+ // save the return URL
+ $CI->charge_data_model->Save($order_id, 'return_url', $return_url);
+
+ $post_url = $this->GetAPIURL($gateway);
+
+ $post = array();
+ $post['version'] = '56.0';
+ $post['method'] = 'SetExpressCheckout';
+ $post['returnurl'] = site_url('callback/paypal_standard/confirm/' . $order_id);
+ $post['cancelurl'] = (!empty($cancel_url)) ? $cancel_url : 'http://www.paypal.com';
+ $post['noshipping'] = '1';
+ $post['addroverride'] = '1';
+ $post['allownote'] = '0';
+ $post['localecode'] = $CI->config->item('locale');
+ $post['solutiontype'] = 'Sole';
+ $post['landingpage'] = 'Billing';
+ $post['channeltype'] = 'Merchant';
+
+ if (isset($customer['email'])) {
+ $post['email'] = $customer['email'];
+ }
+
+ if (isset($customer['first_name'])) {
+ $post['name'] = $customer['first_name'] . ' ' . $customer['last_name'];
+ }
+
+ if (isset($customer['address_1']) and !empty($customer['address_1'])) {
+ $post['SHIPTONAME'] = $customer['first_name'] . ' ' . $customer['last_name'];
+ $post['SHIPTOSTREET'] = $customer['address_1'];
+ $post['SHIPTOSTREET2'] = $customer['address_2'];
+ $post['SHIPTOCITY'] = $customer['city'];
+ $post['SHIPTOSTATE'] = $customer['state'];
+ $post['SHIPTOZIP'] = $customer['postal_code'];
+ $post['SHIPTOCOUNTRYCODE'] = $customer['country'];
+ $post['SHIPTOPHONENUM'] = $customer['phone'];
+ }
+
+ $post['paymentaction'] = 'sale';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['AMT'] = $amount;
+ $post['L_DESC0'] = 'Invoice #' . $order_id;
+ $post['L_AMT0'] = $amount;
+ $post['L_QTY0'] = '1';
+ $post['invnum'] = $order_id;
+ $post['currencycode'] = $gateway['currency'];
+
+ $response = $this->Process($post_url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express Charge Params: ', $post);
+ $this->log_it('PayPal Express Charge Response: ', $response);
+ }
+
+ if (!empty($response['TOKEN'])) {
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['TOKEN']);
+
+ // generate express checkout URL
+ $url = $this->GetExpressCheckoutURL($gateway);
+
+ $url .= '&token=' . $response['TOKEN'];
+
+ $response_array = array(
+ 'not_completed' => TRUE, // don't mark charge as complete
+ 'redirect' => $url, // redirect the user to this address
+ 'charge_id' => $order_id
+ );
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ }
+ else {
+ $response_array = array('reason' => $response['L_ERRORCODE0'] . ' - ' . $response['L_LONGMESSAGE0']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function Recur($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences, $return_url, $cancel_url)
+ {
+ $CI =& get_instance();
+
+ $CI =& get_instance();
+ $CI->load->model('billing/charge_data_model');
+ $CI->load->helper('url');
+
+ $amount = money_format("%!^i",$amount);
+
+ $subscription = $CI->recurring_model->GetRecurring($subscription_id);
+
+ // when we have a free trial, we want to throw out the initial charge.
+ // this happens for all other gateways but because we setup a billing agreement with PayPal,
+ // it's slightly different
+ if ($charge_today === FALSE) {
+ $amount = $subscription['amount'];
+ }
+
+ // save the return and cancel URLs
+ $CI->charge_data_model->Save('r' . $subscription_id, 'return_url', $return_url);
+ $CI->charge_data_model->Save('r' . $subscription_id, 'cancel_url', $cancel_url);
+
+ // save the initial charge amount (it may be different, so we treat it as a separate first charge)
+ $CI->charge_data_model->Save('r' . $subscription_id, 'first_charge', $amount);
+
+ // save the "renewed" subscription
+
+ $post_url = $this->GetAPIURL($gateway);
+
+ // if the total occurrences are 1, we'll send it as a single payment to PayPal
+ // otherwise, we'll send it as a normal PayPal recurring charge
+ if ((int)$total_occurrences == 1 and $charge_today === TRUE) {
+ $CI->charge_data_model->Save('r' . $subscription_id, 'paypal_charge_type', 'single');
+
+ $post = array();
+ $post['version'] = '56.0';
+ $post['method'] = 'SetExpressCheckout';
+ $post['returnurl'] = site_url('callback/paypal_standard/confirm_recur/' . $subscription_id);
+ $post['cancelurl'] = (!empty($cancel_url)) ? $cancel_url : 'http://www.paypal.com';
+ $post['noshipping'] = '1';
+ $post['addroverride'] = '1';
+ $post['allownote'] = '0';
+ $post['localecode'] = $CI->config->item('locale');
+ $post['solutiontype'] = 'Sole';
+ $post['landingpage'] = 'Billing';
+ $post['channeltype'] = 'Merchant';
+
+ if (isset($customer['email'])) {
+ $post['email'] = $customer['email'];
+ }
+
+ if (isset($customer['first_name'])) {
+ $post['name'] = $customer['first_name'] . ' ' . $customer['last_name'];
+ }
+
+ if (isset($customer['address_1']) and !empty($customer['address_1'])) {
+ $post['SHIPTONAME'] = $customer['first_name'] . ' ' . $customer['last_name'];
+ $post['SHIPTOSTREET'] = $customer['address_1'];
+ $post['SHIPTOSTREET2'] = $customer['address_2'];
+ $post['SHIPTOCITY'] = $customer['city'];
+ $post['SHIPTOSTATE'] = $customer['state'];
+ $post['SHIPTOZIP'] = $customer['postal_code'];
+ $post['SHIPTOCOUNTRYCODE'] = $customer['country'];
+ $post['SHIPTOPHONENUM'] = $customer['phone'];
+ }
+
+ $post['paymentaction'] = 'sale';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['AMT'] = $amount;
+
+ $item_description = 'One-time payment';
+
+ if (isset($subscription['plan']['name'])) {
+ $item_description = $subscription['plan']['name'];
+ }
+
+ $post['L_DESC0'] = $item_description;
+ $post['L_AMT0'] = $amount;
+ $post['L_QTY0'] = '1';
+ $post['invnum'] = $subscription_id;
+ $post['currencycode'] = $gateway['currency'];
+ }
+ else {
+ $CI->charge_data_model->Save('r' . $subscription_id, 'paypal_charge_type', 'subscription');
+
+ $post = array();
+ $post['version'] = '56.0';
+ $post['method'] = 'SetExpressCheckout';
+ $post['returnurl'] = site_url('callback/paypal_standard/confirm_recur/' . $subscription_id);
+ $post['cancelurl'] = (!empty($cancel_url)) ? $cancel_url : 'http://www.paypal.com';
+ $post['noshipping'] = '1';
+ $post['addroverride'] = '1';
+ $post['allownote'] = '0';
+ $post['localecode'] = $CI->config->item('locale');
+ $post['solutiontype'] = 'Sole';
+ $post['landingpage'] = 'Billing';
+ $post['channeltype'] = 'Merchant';
+
+ if (isset($customer['email'])) {
+ $post['email'] = $customer['email'];
+ }
+
+ if (isset($customer['first_name'])) {
+ $post['name'] = $customer['first_name'] . ' ' . $customer['last_name'];
+ }
+
+ if (isset($customer['address_1']) and !empty($customer['address_1'])) {
+ $post['SHIPTONAME'] = $customer['first_name'] . ' ' . $customer['last_name'];
+ $post['SHIPTOSTREET'] = $customer['address_1'];
+ $post['SHIPTOSTREET2'] = $customer['address_2'];
+ $post['SHIPTOCITY'] = $customer['city'];
+ $post['SHIPTOSTATE'] = $customer['state'];
+ $post['SHIPTOZIP'] = $customer['postal_code'];
+ $post['SHIPTOCOUNTRYCODE'] = $customer['country'];
+ $post['SHIPTOPHONENUM'] = $customer['phone'];
+ }
+
+ $post['PAYMENTACTION'] = 'sale';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['AMT'] = $amount;
+ $post['invnum'] = $subscription_id;
+ $post['currencycode'] = $gateway['currency'];
+ $post['L_BILLINGTYPE0'] = 'RecurringPayments';
+
+ $item_description = 'Recurring payment';
+
+ $subscription = $CI->recurring_model->GetRecurring($subscription_id);
+
+ if (isset($subscription['plan']['name'])) {
+ $item_description = $subscription['plan']['name'];
+ }
+
+ $post['L_DESC0'] = $item_description;
+ $post['L_AMT0'] = $amount;
+ $post['L_QTY0'] = '1';
+
+ // handle first charges unless there's a free trial
+ if ($charge_today === TRUE) {
+ // first recurring charge won't start until after the first interval
+ // we'll run an instant payment first
+ // old start date
+ $adjusted_start_date = TRUE;
+ $start_date = date('Y-m-d',strtotime($start_date)+(60*60*24*$interval));
+ }
+
+ // get true recurring rate, first
+ // $subscription loaded above
+
+ $description = ($subscription['amount'] != $amount) ? 'Initial charge: ' . $gateway['currency'] . $amount . ', then ' : '';
+ $description .= $gateway['currency'] . money_format("%!^i",$subscription['amount']) . ' every ' . $interval . ' days until ' . date('Y-m-d', strtotime($subscription['end_date']));
+ if ($charge_today === FALSE) {
+ $description .= ' (free trial ends ' . $start_date . ')';
+ }
+ $post['L_BILLINGAGREEMENTDESCRIPTION0'] = $description;
+
+ $CI->charge_data_model->Save('r' . $subscription_id, 'profile_description', $description);
+ }
+
+ $response = $this->Process($post_url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express Recur Params: ', $post);
+ $this->log_it('PayPal Express Recur Response: ', $response);
+ }
+
+ if (!empty($response['TOKEN'])) {
+ // generate express checkout URL
+ $url = $this->GetExpressCheckoutURL($gateway);
+
+ $url .= '&token=' . $response['TOKEN'];
+
+ $response_array = array(
+ 'not_completed' => TRUE, // don't mark charge as complete
+ 'redirect' => $url, // redirect the user to this address
+ 'recurring_id' => $subscription_id
+ );
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ }
+ else {
+ $response_array = array('reason' => $response['L_ERRORCODE0'] . ' - ' . $response['L_LONGMESSAGE0']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function Process($url, $post_data)
+ {
+ $CI =& get_instance();
+
+ $data = '';
+
+ // Build the data string for the request body
+ foreach($post_data as $key => $value)
+ {
+ if(!empty($value))
+ {
+ $data .= strtoupper($key) . '=' . urlencode($value) . '&';
+ }
+ }
+
+ // remove the extra ampersand
+ $data = substr($data, 0, strlen($data) - 1);
+
+ // setting the curl parameters.
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_VERBOSE, 1);
+
+ // turning off the server and peer verification(TrustManager Concept).
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
+
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_POST, 1);
+
+ // setting the nvpreq as POST FIELD to curl
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+
+ // getting response from server
+ $response = curl_exec($ch);
+
+ // Throw an error if we can't continue. Will help in debugging.
+ if (curl_error($ch))
+ {
+ show_error(curl_error($ch));
+ }
+
+ $response = $this->response_to_array($response);
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function CancelRecurring($subscription, $gateway)
+ {
+ $CI =& get_instance();
+
+ // is this a real subscription, or an occurrences = 1 situation?
+ // get charge data
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get('r' . $subscription['subscription_id']);
+
+ // we have to check for the existence of this key because older subscriptions
+ // prior to this version (1.78) won't include this data
+ if (isset($data['paypal_charge_type']) and $data['paypal_charge_type'] != 'subscription') {
+ return TRUE;
+ }
+
+ $CI->load->model('billing/recurring_model');
+
+ $post_url = $this->GetAPIURL($gateway);
+
+ $post = array();
+ $post['version'] = '60';
+ $post['method'] = 'ManageRecurringPaymentsProfileStatus';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['profileid'] = $subscription['api_customer_reference'];
+ $post['action'] = 'Cancel';
+
+ $post_response = $this->Process($post_url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express CancelRecurring Params: ', $post);
+ $this->log_it('PayPal Express CancelRecurring Response: ', $post_response);
+ }
+
+ if($post_response['ACK'] == 'Success') {
+ $response = TRUE;
+ } else {
+ $response = FALSE;
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function UpdateRecurring($gateway, $subscription, $customer, $params)
+ {
+ $CI =& get_instance();
+
+ // is this a real subscription, or an occurrences = 1 situation?
+ // get charge data
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get('r' . $subscription['subscription_id']);
+
+ // we have to check for the existence of this key because older subscriptions
+ // prior to this version (1.78) won't include this data
+ if (isset($data['paypal_charge_type']) and $data['paypal_charge_type'] != 'subscription') {
+ return FALSE;
+ }
+
+ $CI->load->model('billing/recurring_model');
+
+ $post_url = $this->GetAPIURL($gateway);
+
+ $post = array();
+ $post['version'] = '58.0';
+ $post['method'] = 'UpdateRecurringPaymentsProfile';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['profileid'] = $subscription['api_customer_reference'];
+
+ if(isset($params['amount'])) {
+ $post['currencycode'] = $gateway['currency'];
+ $post['amt'] = $params['amount'];
+ }
+
+ if(isset($params['customer_id'])){
+
+ $post['firstname'] = $customer['first_name'];
+ $post['lastname'] = $customer['last_name'];
+ $post['street'] = $customer['address_1'];
+
+ if($customer['address_1'] != '') {
+ $post['street'] .= ' '.$customer['address_2'];
+ }
+
+ $post['city'] = $customer['city'];
+ $post['state'] = $customer['state'];
+ $post['zip'] = $customer['postal_code'];
+ }
+
+ $post_response = $this->Process($post_url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express UpdateRecurring Params: ', $post);
+ $this->log_it('PayPal Express UpdateRecurring Response: ', $response);
+ }
+
+ if($post_response['ACK'] == 'Success') {
+ $response = TRUE;
+ } else {
+ $response = FALSE;
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $params);
+ }
+
+ //--------------------------------------------------------------------
+
+ function ChargeRecurring($gateway, $params)
+ {
+ // is this a real subscription, or an occurrences = 1 situation?
+ // get charge data
+ $CI =& get_instance();
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get('r' . $params['subscription_id']);
+
+ // we have to check for the existence of this key because older subscriptions
+ // prior to this version (1.78) won't include this data
+ if (isset($data['paypal_charge_type']) and $data['paypal_charge_type'] != 'subscription') {
+ $response = array('success' => FALSE, 'reason' => 'Not a subscription. Occurrences were zero when this subscription was created.');
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express ChargeRecurring (Old Sub) Params: ', $params);
+ $this->log_it('PayPal Express ChargeRecurring (Old Sub) Response: ', $response);
+ }
+
+ return $response;
+ }
+
+ $CI->load->library('billing/transaction_log');
+ $CI->load->model('billing/recurring_model');
+
+ $details = $this->GetProfileDetails($gateway, $params);
+ if (!$details) {
+ // if we didn't retrieve the profile properly, we'd rather let the subscription
+ // go then cancel it due to a one-time connection issue
+ return array('success' => TRUE);
+ }
+
+ /*
+ * We used to check for failed payments but PayPal only marks them failed after like
+ 15 days...
+ */
+ $failed_payments = $details['FAILEDPAYMENTCOUNT'];
+ $status = $details['STATUS'];
+
+ $response = array();
+
+ if ($status != 'Cancelled' and (int)$failed_payments === 0) {
+ $response['success'] = TRUE;
+
+ // should we cancel this subscription? i.e., will it expire before the next renew?
+ // this is only important because PayPal's charge scheduling sometimes jumps the gun
+ if (strtotime($params['end_date']) <= (strtotime($params['next_charge']) + (60*60*24*$params['charge_interval']))) {
+ // silently cancel the subscription
+ $CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'paypal_pre_cancel', FALSE, __FILE__, __LINE__);
+ $next_charge = $CI->recurring_model->GetNextChargeDate($params['subscription_id'], $params['next_charge']);
+ $CI->db->update('subscriptions', array('next_charge' => $next_charge), array('subscription_id' => $params['subscription_id']));
+ $CI->recurring_model->CancelRecurring($params['subscription_id'], TRUE);
+ }
+ } else {
+ if ((int)$failed_payments > 0) {
+ $CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'paypal_charge_recurring_response', array('response' => 'Failed payments: ' . $failed_payments), __FILE__, __LINE__);
+ }
+ else {
+ $CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'paypal_charge_recurring_response', array('response' => 'Status: ' . $status), __FILE__, __LINE__);
+ }
+
+ $response['success'] = FALSE;
+ $response['reason'] = "The charge has failed.";
+ }
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express ChargeRecurring Profile Details: ', $details);
+ $this->log_it('PayPal Express ChargeRecurring Response: ', $response);
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ function Callback_confirm ($gateway, $charge, $params) {
+ $CI =& get_instance();
+
+ $url = $this->GetAPIURL($gateway);
+
+ $post = array();
+ $post['method'] = 'GetExpressCheckoutDetails';
+ $post['token'] = $params['token'];
+ $post['version'] = '56.0';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+
+ $response = $this->Process($url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express Callback Confirm Params: ', $post);
+ $this->log_it('PayPal Express Callback Confirm Response: ', $response);
+ }
+
+ if (isset($response['TOKEN']) and $response['TOKEN'] == $params['token']) {
+ // we're good
+
+ // complete the payment
+ $post = $response; // most of the data is from here
+ unset($post['NOTE']);
+
+ $post['METHOD'] = 'DoExpressCheckoutPayment';
+ $post['TOKEN'] = $response['TOKEN'];
+ $post['PAYMENTACTION'] = 'Sale';
+ $post['version'] = '56.0';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+
+ $response = $this->Process($url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express DoExpressCheckoutPayment Request: ', $post);
+ $this->log_it('PayPal Express DoExpressCheckoutPayment Response: ', $response);
+ }
+
+ if (isset($response['PAYMENTSTATUS']) and ($response['PAYMENTSTATUS'] == 'Completed' or $response['PAYMENTSTATUS'] == 'Pending' or $response['PAYMENTSTATUS'] == 'Processed')) {
+ // we're good
+
+ // save authorization (transaction id #)
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($charge['id'], $response['TRANSACTIONID']);
+
+ $CI->charge_model->SetStatus($charge['id'], 1);
+
+ // get return URL from original request
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get($charge['id']);
+
+ // redirect back to user's site
+ header('Location: ' . $data['return_url']);
+ die();
+ }
+ }
+
+ die(show_error('Your PayPal payment has failed. Please contact the site administrator.'));
+ }
+
+ //--------------------------------------------------------------------
+
+ function Callback_confirm_recur ($gateway, $subscription, $params) {
+ $CI =& get_instance();
+
+ // get charge data
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get('r' . $subscription['id']);
+
+ // this gets complex below, so we'll track the general success of this process
+ // with a simple boolean variable
+ $process_status = FALSE;
+
+ $url = $this->GetAPIUrl($gateway);
+
+ $post = array();
+ $post['method'] = 'GetExpressCheckoutDetails';
+ $post['token'] = $params['token'];
+ $post['version'] = '56.0';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+
+ $response = $this->Process($url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express Callback Confirm Recur Params: ', $post);
+ $this->log_it('PayPal Express Charge Callback Confirm Response: ', $response);
+ }
+
+ if (isset($response['TOKEN']) and $response['TOKEN'] == $params['token']) {
+ // tokens match. this is a legitimate PayPal request
+
+ // do we need a first charge?
+ if (date('Y-m-d',strtotime($subscription['start_date'])) == date('Y-m-d', strtotime($subscription['date_created']))) {
+ $CI->load->model('billing/charge_model');
+
+ // get the first charge amount (it may be different)
+ $first_charge_amount = (isset($data['first_charge'])) ? $data['first_charge'] : $subscription['amount'];
+ $first_charge_amount = (float)$first_charge_amount;
+
+ if (!empty($first_charge_amount)) {
+ $customer_id = (isset($subscription['customer']['id'])) ? $subscription['customer']['id'] : FALSE;
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $first_charge_amount, array(), $subscription['id'], $customer_id);
+
+ // yes, the first charge is today
+ $post = $response; // most of the data is from here
+ unset($post['NOTE']);
+
+ $post['METHOD'] = 'DoExpressCheckoutPayment';
+ $post['TOKEN'] = $response['TOKEN'];
+ $post['PAYMENTACTION'] = 'Sale';
+ $post['version'] = '56.0';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+
+ $response_charge = $this->Process($url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express Callback Confirm Recur - First Charge Params: ', $post);
+ $this->log_it('PayPal Express Callback Confirm Recur - First Charge Response: ', $response_charge);
+ }
+
+ if (!isset($response_charge['PAYMENTSTATUS']) or ($response_charge['PAYMENTSTATUS'] != 'Completed' and $response_charge['PAYMENTSTATUS'] != 'Pending' and $response_charge['PAYMENTSTATUS'] != 'Processed')) {
+ die(show_error('Your PayPal payment has failed (initial setup). Please contact the site administrator.'));
+ }
+ else {
+ // create today's order
+ // we assume it's good because the profile is OK
+
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response_charge['TRANSACTIONID']);
+
+ $CI->charge_model->SetStatus($order_id, 1);
+ }
+
+ // we'll also adjust the profile start date
+ $adjusted_start_date = TRUE;
+ $subscription['start_date'] = date('Y-m-d',strtotime($subscription['start_date'])+(60*60*24*$subscription['interval']));
+ }
+ }
+
+ // if this was sent to PayPal as a recurring payment, we'll create the profile here
+ if ($data['paypal_charge_type'] == 'subscription') {
+ // continue with creating payment profile
+ $post = $response; // most of the data is from here
+ unset($post['NOTE']);
+
+ $post['METHOD'] = 'CreateRecurringPaymentsProfile';
+ $post['VERSION'] = '60';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['TOKEN'] = $response['TOKEN'];
+ $post['DESC'] = $data['profile_description'];
+ $post['PROFILESTARTDATE'] = date('c',strtotime($subscription['start_date']));
+ $post['BILLINGPERIOD'] = 'Day';
+ $post['BILLINGFREQUENCY'] = $subscription['interval'];
+ $post['AMT'] = $subscription['amount'];
+
+ $response_sub = $this->Process($url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express Create Payment Profile Params: ', $post);
+ $this->log_it('PayPal Express Create Payment Profile Response: ', $response_sub);
+ }
+
+
+ if (isset($response_sub['PROFILEID'])) {
+ // success!
+ $CI->recurring_model->SaveApiCustomerReference($subscription['id'], $response_sub['PROFILEID']);
+
+ $process_status = TRUE;
+ }
+ }
+ else {
+ // we know we are good because the first charge was executed before and, if it failed
+ // we'd have die()'d above
+ $process_status = TRUE;
+ }
+
+ if ($process_status === TRUE) {
+ // success!
+ $order_id = (isset($order_id)) ? $order_id : FALSE;
+
+ $CI->recurring_model->SetActive($subscription['id']);
+
+ // hook
+ $CI->load->library('app_hooks');
+ $CI->app_hooks->data('subscription', $subscription['id']);
+ $CI->app_hooks->trigger('subscription_new', $subscription['id']);
+
+ // trip a recurring charge?
+ if ($order_id) {
+ // hook
+ $CI->app_hooks->data('invoice', $order_id);
+ $CI->app_hooks->trigger('subscription_charge', $order_id, $subscription['id']);
+ }
+
+ $CI->app_hooks->reset();
+
+ // redirect back to user's site
+ header('Location: ' . $data['return_url']);
+ die();
+ }
+ else {
+ die(show_error('Your PayPal payment has failed (profile error). Please contact the site administrator.'));
+ }
+ }
+
+ die(show_error('Your PayPal payment has failed. Please contact the site administrator.'));
+ }
+
+ //--------------------------------------------------------------------
+
+ function GetProfileDetails($gateway, $params)
+ {
+ $CI =& get_instance();
+ $CI->load->model('billing/recurring_model');
+
+ $post_url = $this->GetAPIURL($gateway);
+
+ $post = array();
+ $post['version'] = '60';
+ $post['method'] = 'GetRecurringPaymentsProfileDetails';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['profileid'] = $params['api_customer_reference'];
+
+ $post_response = $this->Process($post_url, $post);
+
+ if ($this->debug)
+ {
+ $this->log_it('PayPal Express GetProfileDetails Params: ', $post);
+ $this->log_it('PayPal Express GetProfileDetails Response: ', $post_response);
+ }
+
+ if ($post_response['ACK'] == 'Success') {
+ return $post_response;
+ } else {
+ return FALSE;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ private function GetAPIURL ($gateway) {
+ if ($gateway['mode'] == 'test') {
+ return $gateway['url_test'];
+ }
+ else {
+ return $gateway['url_live'];
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ private function GetExpressCheckoutURL ($gateway) {
+ if ($gateway['mode'] == 'test') {
+ return $gateway['arb_url_test'];
+ }
+ else {
+ return $gateway['arb_url_live'];
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ private function response_to_array($string)
+ {
+ $string = urldecode($string);
+ $pairs = explode('&', $string);
+ $values = array();
+
+ foreach($pairs as $pair)
+ {
+ list($key, $value) = explode('=', $pair);
+ $values[$key] = $value;
+ }
+
+ return $values;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: log_it()
+
+ Logs the transaction to a file. Helpful with debugging callback
+ transactions, since we can't actually see what's going on.
+
+ Parameters:
+ $heading - A string to be placed above the resutls
+ $params - Typically an array to print_r out so that we can inspect it.
+ */
+ public function log_it($heading, $params)
+ {
+ $file = FCPATH .'writeable/gateway_log.txt';
+
+ $content = '';
+ $content .= "# $heading\n";
+ $content .= date('Y-m-d H:i:s') ."\n\n";
+ $content .= print_r($params, true);
+ file_put_contents($file, $content, FILE_APPEND);
+ }
+
+}
diff --git a/app/modules/billing/libraries/payment/sagepay.php b/app/modules/billing/libraries/payment/sagepay.php
new file mode 100644
index 00000000..e5d01d98
--- /dev/null
+++ b/app/modules/billing/libraries/payment/sagepay.php
@@ -0,0 +1,443 @@
+settings = $this->Settings();
+ }
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'SagePay';
+ $settings['class_name'] = 'sagepay';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'SagePay is the premier merchant account provider for the United Kingdom.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '£0';
+ $settings['monthly_fee'] = '£20';
+ $settings['transaction_fee'] = '10p';
+ $settings['purchase_link'] = 'https://support.protx.com/apply/default.aspx?PartnerID=D16D4B72-87D5-4E97-A743-B45078E146CB';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 0;
+ $settings['requires_customer_information'] = 1;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array(
+ 'enabled',
+ 'mode',
+ 'vendor',
+ 'currency',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_dc',
+ 'accept_amex'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'test' => 'Test Mode',
+ 'simulator' => 'Simulator'
+ )
+ ),
+ 'vendor' => array(
+ 'text' => 'Vendor',
+ 'type' => 'text'
+ ),
+ 'currency' => array(
+ 'text' => 'Currency',
+ 'type' => 'select',
+ 'options' => array(
+ 'GBP' => 'GBP - Pound Sterling',
+ 'EUR' => 'EUR - Euro',
+ 'USD' => 'USD - US Dollar',
+ 'AUD' => 'AUD - Australian Dollar',
+ 'CAD' => 'CAD - Canadian Dollar',
+ 'CHF' => 'CHF - Swiss Franc',
+ 'DKK' => 'DKK - Danish Krone',
+ 'HKD' => 'HKD - Hong Kong Dollar',
+ 'IDR' => 'IDR - Rupiah',
+ 'JPY' => 'JPY - Yen',
+ 'LUF' => 'LUF - Luxembourg Franc',
+ 'NOK' => 'NOK - Norwegian Krone',
+ 'NZD' => 'NZD - New Zealand Dollar',
+ 'SEK' => 'SEK - Swedish Krona',
+ 'SGD' => 'SGD - Singapore Dollar',
+ 'TRL' => 'TRL - Turkish Lira'
+ )
+ ),
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_dc' => array(
+ 'text' => 'Accept Diner\'s Club?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ function TestConnection($gateway)
+ {
+ // There's no way to test the connection at this point
+ return TRUE;
+ }
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card, $txtype = 'PAYMENT')
+ {
+ $CI =& get_instance();
+
+ $post_url = $this->GetAPIUrl($gateway);
+
+ // get card type in proper format
+ switch($credit_card['card_type']) {
+ case 'visa';
+ $card_type = 'VISA';
+ break;
+ case 'mc';
+ $card_type = 'MC';
+ break;
+ case 'discover';
+ $card_type = 'DC';
+ break;
+ case 'amex';
+ $card_type = 'AMEX';
+ break;
+ }
+
+ $post_values = array(
+ "VPSProtocol" => "2.23",
+ "TxType" => $txtype,
+ "Vendor" => $gateway['vendor'],
+ "VendorTxCode" => 'opengateway-' . $order_id,
+ "Amount" => $amount,
+ "Currency" => $gateway['currency'],
+ "Description" => "API Payment at " . date('Y-m-d H:i:s') . " via " . $CI->config->item('server_name'),
+ "CardHolder" => $credit_card['name'],
+ "CardNumber" => $credit_card['card_num'],
+ "ExpiryDate" => str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT) . substr($credit_card['exp_year'],-2,2),
+ "CardType" => $card_type,
+ "Apply3DSecure" => "2" // No 3DSecure checks, ever
+ );
+
+ if(isset($credit_card['cvv'])) {
+ $post_values['CV2'] = $credit_card['cvv'];
+ }
+
+ if (isset($customer['customer_id'])) {
+ $post_values['BillingFirstNames'] = $customer['first_name'];
+ $post_values['BillingSurname'] = $customer['last_name'];
+ $post_values['BillingAddress1'] = $customer['address_1'];
+ if (isset($customer['address_2']) and !empty($customer['address_2'])) {
+ $post_values['BillingAddress2'] = ' - '.$customer['address_2'];
+ }
+ $post_values['BillingCity'] = $customer['city'];
+ if (!empty($customer['state']) and !empty($customer['country']) and ($customer['country'] == 'US')) {
+ // only for North American customers
+ $post_values['BillingState'] = $customer['state'];
+ }
+ $post_values['BillingPostCode'] = $customer['postal_code'];
+ $post_values['BillingCountry'] = $customer['country'];
+ if (!empty($customer['phone'])) {
+ $post_values['BillingPhone'] = $customer['phone'];
+ }
+
+ if (!empty($customer['email'])) {
+ $post_values['CustomerEMail'] = $customer['email'];
+ }
+
+ if (!empty($customer['ip_address'])) {
+ $post_values['ClientIPAddress'] = $customer['ip_address'];
+ }
+
+ // duplicate for delivery
+ $post_values['DeliveryFirstNames'] = $customer['first_name'];
+ $post_values['DeliverySurname'] = $customer['last_name'];
+ $post_values['DeliveryAddress1'] = $customer['address_1'];
+ if (isset($customer['address_2']) and !empty($customer['address_2'])) {
+ $post_values['DeliveryAddress2'] = ' - '.$customer['address_2'];
+ }
+ $post_values['DeliveryCity'] = $customer['city'];
+ if (!empty($customer['state']) and !empty($customer['country']) and ($customer['country'] == 'US')) {
+ // only for North American customers
+ $post_values['DeliveryState'] = $customer['state'];
+ }
+ $post_values['DeliveryPostCode'] = $customer['postal_code'];
+ $post_values['DeliveryCountry'] = $customer['country'];
+ if (!empty($customer['phone'])) {
+ $post_values['DeliveryPhone'] = $customer['phone'];
+ }
+ }
+
+ $response = $this->Process($order_id, $post_url, $post_values);
+
+ if ($response['success'] == TRUE){
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ } else {
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE, $return_url = '', $cancel_url = '')
+ {
+ $CI =& get_instance();
+
+ // if a payment is to be made today, process it.
+ if ($charge_today === TRUE) {
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ $response = $this->Charge($order_id, $gateway, $customer, $amount, $credit_card);
+
+ if ($response['response_code'] == '1') {
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ } else {
+ // we need to process an initial AUTHENTICATE transaction in order to send REPEATs later
+
+ // generate a fake random Order ID - this isn't a true order
+ $order_id = rand(100000,1000000);
+ $response = $this->Charge($order_id, $gateway, $customer, $amount, $credit_card, 'AUTHENTICATE');
+
+ if ($response['response_code'] == '1') {
+ $response_array = array('recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+
+ $response_array = array('reason' => $response['reason']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ }
+
+ // let's save the transaction details for future REPEATs
+
+ // for SagePay:
+ // api_customer_reference = VPSTxId
+ // api_payment_reference = VendorTxCode|VendorTxAuthNo
+ // api_auth_number = SecurityKey
+
+ // these authorizations were saved during $this->Process()
+ if ($response['response_code'] != '2') {
+ $authorizations = $CI->order_authorization_model->GetAuthorization($order_id);
+
+ $CI->recurring_model->SaveApiCustomerReference($subscription_id, $authorizations->tran_id);
+ $CI->recurring_model->SaveApiPaymentReference($subscription_id, $authorizations->order_id . '|' . $authorizations->authorization_code);
+ $CI->recurring_model->SaveApiAuthNumber($subscription_id, $authorizations->security_key);
+ }
+
+ return $response;
+ }
+
+ function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $order_id, $params['api_customer_reference'], $params['api_payment_reference'], $params['api_auth_number'], $params['amount']);
+ }
+
+ function ChargeRecurring($gateway, $order_id, $VPSTxId, $VendorTxCodeVendorTxAuthNo, $SecurityKey, $amount)
+ {
+ $CI =& get_instance();
+
+ list($VendorTxCode,$VendorTxAuthNo) = explode('|',$VendorTxCodeVendorTxAuthNo);
+
+ $post_url = $this->GetAPIUrl($gateway, 'repeat');
+
+ $post_values = array(
+ "VPSProtocol" => "2.23",
+ "TxType" => 'REPEAT',
+ "Vendor" => $gateway['vendor'],
+ "VendorTxCode" => 'opengateway-' . $order_id,
+ "Amount" => $amount,
+ "Currency" => $gateway['currency'],
+ "Description" => "API Payment at " . date('Y-m-d H:i:s'),
+ "RelatedVPSTxId" => $VPSTxId,
+ "RelatedVendorTxCode" => 'opengateway-' . $VendorTxCode,
+ "RelatedTxAuthNo" => $VendorTxAuthNo,
+ "RelatedSecurityKey" => $SecurityKey,
+ "AccountType" => "C"
+ );
+
+ $response = $this->Process($order_id, $post_url, $post_values);
+
+ if ($response['success'] == TRUE){
+ return $response;
+ } else {
+ $response['success'] = FALSE;
+ $response['reason'] = $response['reason'];
+
+ return $response;
+ }
+ }
+
+ function UpdateRecurring()
+ {
+ return TRUE;
+ }
+
+ function Process($order_id, $post_url, $post_values)
+ {
+ $CI =& get_instance();
+ $CI->load->model('billing/charge_model');
+
+ // build NVP post string
+ $post_string = "";
+ foreach($post_values as $key => $value) {
+ $post_string .= "$key=" . urlencode( $value ) . "&";
+ }
+ $post_string = rtrim($post_string, "& ");
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_POSTFIELDS, $post_string); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, TRUE); // uncomment this line if you get no gateway response.
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close ($request); // close curl object
+
+ $response_lines = explode("\r\n",$post_response);
+
+ if (!is_array($response_lines)) {
+ // we didn't receive back a series of newlines like we thought we would
+ $response = array();
+ $response['success'] = FALSE;
+
+ return $response;
+ }
+
+ // put into array
+ $response = array();
+ foreach ($response_lines as $line) {
+ if (!empty($line)) {
+ list($name,$value) = explode('=',$line);
+ $response[$name] = $value;
+ }
+ }
+
+ // the OK message changes depending on the type
+ if ($post_values['TxType'] == 'PAYMENT') {
+ $ok_message = 'OK';
+ }
+ elseif ($post_values['TxType'] == 'REPEAT') {
+ $ok_message = 'OK';
+ }
+ elseif ($post_values['TxType'] == 'AUTHENTICATE') {
+ $ok_message = 'REGISTERED';
+ }
+
+ // did it process properly?
+ if($response['Status'] == $ok_message) {
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['VPSTxId'], $response['TxAuthNo'], $response['SecurityKey']);
+ $CI->charge_model->SetStatus($order_id, 1);
+
+ $response['success'] = TRUE;
+ } else {
+ $CI->load->model('billing/charge_model');
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response['success'] = FALSE;
+ $response['reason'] = $response['StatusDetail'];
+ }
+
+ return $response;
+ }
+
+ function GetAPIUrl($gateway, $mode = FALSE) {
+ if ($mode == FALSE) {
+ switch($gateway['mode']) {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ case 'simulator':
+ $post_url = $gateway['url_dev'];
+ break;
+ }
+ }
+ elseif ($mode == 'repeat') {
+ switch($gateway['mode']) {
+ case 'live':
+ $post_url = $gateway['arb_url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['arb_url_test'];
+ break;
+ case 'simulator':
+ $post_url = $gateway['arb_url_dev'];
+ break;
+ }
+ }
+
+ return $post_url;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/stripe_gw.php b/app/modules/billing/libraries/payment/stripe_gw.php
new file mode 100644
index 00000000..820c02bf
--- /dev/null
+++ b/app/modules/billing/libraries/payment/stripe_gw.php
@@ -0,0 +1,502 @@
+settings = $this->Settings();
+
+ // Load the Stripe lib.
+ require_once(APPPATH .'modules/billing/libraries/stripe/Stripe.php');
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: Settings()
+
+ Provides a single place to specify the default settings for the gateway.
+ */
+ public function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'Stripe';
+ $settings['class_name'] = 'stripe_gw';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'Stripe makes it easy to start accepting credit cards on the web today. Requires an SSL connection. US only.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$0';
+ $settings['monthly_fee'] = '$0';
+ $settings['transaction_fee'] = '2.9% + $0.30';
+ $settings['purchase_link'] = 'https://stripe.com';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 1;
+ $settings['requires_customer_ip'] = 1;
+ $settings['required_fields'] = array(
+ 'enabled',
+ 'mode',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_amex',
+ 'test_api_key',
+ 'live_api_key'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'test' => 'Testing'
+ )
+ ),
+ 'test_api_key' => array(
+ 'text' => 'Test API Key',
+ 'type' => 'text'
+ ),
+ 'live_api_key' => array(
+ 'text' => 'Live API Key',
+ 'type' => 'text'
+ ),
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: TestConnection()
+
+ Tests that the user provided gateway information is correct.
+ To test, we attempt to create a new customer. If successful,
+ the customer is deleted immediately.
+
+ Parameters:
+ $gateway - An array of gateway information.
+
+ Returns:
+ True/False
+ */
+ public function TestConnection($gateway)
+ {
+ $key = $gateway[$gateway['mode'] .'_api_key'];
+
+ Stripe::setApiKey($key);
+
+ $data = array(
+ 'email' => 'test1@gmail.com'
+ );
+
+ try{
+ $customer = Stripe_Customer::create($data);
+
+ $customer->delete();
+
+ return TRUE;
+ }
+ catch (Exception $e)
+ {
+ return FALSE;
+ }
+
+ return false;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: Charge()
+
+ Performs a one-time charge.
+
+ Parameters:
+ $order_id - The internal OpenGateway order id.
+ $gateway - An array of gateway information
+ $customer - An array with customer information
+ $amount - The amount of the charge
+ $credit_card - An array of credit card information
+
+ Returns:
+ $response - A TransactionResponse object.
+ */
+ public function Charge($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ $key = $gateway[$gateway['mode'] .'_api_key'];
+ Stripe::setApiKey($key);
+
+ $amount = $amount * 100; // We need it in cents
+
+ $data = array(
+ 'amount' => $amount,
+ 'currency' => 'usd', // Currently, the only available option
+ 'card' => array(
+ 'number' => $credit_card['card_num'],
+ 'exp_month' => $credit_card['exp_month'],
+ 'exp_year' => $credit_card['exp_year'],
+ 'cvc' => $credit_card['cvv'],
+ 'name' => $credit_card['name'],
+ 'address_line1' => isset($customer['address1']) ? $customer['address1'] : null,
+ 'address_line2' => isset($customer['address2']) ? $customer['address2'] : null,
+ 'address_zip' => isset($customer['postal_code']) ? $customer['postal_code'] : null,
+ 'address_state' => isset($customer['state']) ? $customer['state'] : null,
+ 'address_country' => isset($customer['country']) ? $customer['country'] : null,
+ ),
+ 'description' => 'Charge for '. $customer['email']
+ );
+
+ try {
+ // Successful transaction
+ $response = Stripe_Charge::create($data);
+
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response->id, $response->id);
+ $CI->charge_model->SetStatus($order_id, 1);
+
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ }
+ catch (Exception $e)
+ {
+ // Failed Transaction
+ $CI->load->model('billing/charge_model');
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response_array = array('reason' => $e->getMessage());
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: Refund()
+
+ Refunds the full amount of a charge.
+
+ Parameters:
+ $gateway - An array of the gateway information
+ $charge - An array of charge information
+ $authorization -
+ */
+ public function Refund($gateway, $charge, $authorization)
+ {
+ $CI =& get_instance();
+
+ $key = $gateway[$gateway['mode'] .'_api_key'];
+ Stripe::setApiKey($key);
+
+ try {
+ $charge = Stripe_Charge::retrieve($authorization->tran_id);
+ $charge->Refund();
+
+ return TRUE;
+ }
+ catch (Exception $e)
+ {
+ return FALSE;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: Recur()
+
+ Called when an initial Recur charge comes through to create a subscription.
+
+ Parameters:
+ $gateway - An array of gateway information
+ $customer - An array of customer information
+ $amount - A float with the amount to charge
+ $charge_today - Boolean
+ $start_date - The day the subscription should start
+ $end_date - The day the subscription should end
+ $interval - The number of days between subscription charges
+ $credit_card - An array with the credit card information
+ $subscription_id - An int with the internal subscription id
+ $total_occurences - The total number of charges, or FALSE.
+ */
+ public function Recur($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE)
+ {
+ $CI =& get_instance();
+
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $customer['customer_id'] = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $CI->input->ip_address());
+
+ // Create the recurring seed - Done this way for token based API's.
+ $response = $this->CreateProfile($gateway, $customer, $credit_card, $subscription_id, $amount, $order_id);
+
+ // Process today's payment
+ if ($charge_today === TRUE) {
+ $response = $this->ChargeRecurring($gateway, $order_id, $response['customer_id'], $amount);
+
+ if($response['success'] === TRUE){
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+ $CI->charge_model->SetStatus($order_id, 0);
+
+ $response_array = array('reason' => $response['ResponseSummary']['StatusDescription']);
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ } else {
+ $response = $CI->response->TransactionResponse(100, array('recurring_id' => $subscription_id));
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: CreateProfile()
+
+ Creates a new customer on at Stripe and attaches a credit card to their account.
+
+ Parameters:
+ $gateway - An array of gateway information.
+ $customer - An array of customer information.
+ $credit_card - An array of credit card information.
+ $subscription_id - An INT with the internal ID of the subscription.
+ $amount - A float with the amount of the charge.
+ $order_id - An INT with the internal id of the order.
+ */
+ public function CreateProfile($gateway, $customer, $credit_card, $subscription_id, $amount, $order_id)
+ {
+ $CI =& get_instance();
+
+ $key = $gateway[$gateway['mode'] .'_api_key'];
+ Stripe::setApiKey($key);
+
+ $data = array(
+ 'email' => $customer['email'],
+ 'description' => isset($customer['first_name']) ? $customer['first_name'] .' '. $customer['last_name'] : '',
+ 'card' => array(
+ 'number' => $credit_card['card_num'],
+ 'exp_month' => $credit_card['exp_month'],
+ 'exp_year' => $credit_card['exp_year'],
+ 'cvc' => $credit_card['cvv'],
+ 'name' => isset($credit_card['name']) ? $credit_card['name'] : null,
+ 'address_line1' => isset($customer['address1']) ? $customer['address1'] : null,
+ 'address_line2' => isset($customer['address2']) ? $customer['address2'] : null,
+ 'address_zip' => isset($customer['postal_code']) ? $customer['postal_code'] : null,
+ 'address_state' => isset($customer['state']) ? $customer['state'] : null,
+ 'address_country' => isset($customer['country']) ? $customer['country'] : null,
+ )
+ );
+
+ $response = array();
+
+ try {
+ // Successful Creation
+ $customer = Stripe_Customer::create($data);
+
+ $response['success'] = true;
+ $response['customer_id'] = $customer->id;
+
+ // Save the Auth information
+ $CI->load->model('billing/recurring_model');
+ $CI->recurring_model->SaveApiCustomerReference($subscription_id, $customer->id);
+ }
+ catch (Exception $e)
+ {
+ // Failed creation
+ $response['success'] = false;
+ $response['reason'] = $e->getMessage();
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: ChargeRecurring
+
+ This method handles the actual charging of a recurring payment,
+ both for the first-time time (from Recur) and remaining payments
+ (from AutoRecurringCharge).
+
+ Parameters:
+ $gateway - An array of gateway information.
+ $order_id - An INT with the internal order_id.
+ $customer_id - An INT with the internal customer id.
+ $amount - A float with the amount of the charge.
+ $occurrences - The total number of payments.
+ */
+ public function ChargeRecurring($gateway, $order_id, $customer_id, $amount, $occurences=0)
+ {
+ $CI =& get_instance();
+
+ $key = $gateway[$gateway['mode'] .'_api_key'];
+ Stripe::setApiKey($key);
+
+ $data = array(
+ 'amount' => $amount * 100,
+ 'currency' => 'usd',
+ 'customer' => $customer_id
+ );
+
+ $response = array();
+
+ try {
+ $charge = Stripe_Charge::create($data);
+
+ $response['success'] = TRUE;
+ $response['transaction_num'] = $charge->id;
+ $response['auth_code'] = $charge->id;
+
+ // Save the Auth information
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $response['transaction_num'], $response['auth_code']);
+ } catch (Exception $e)
+ {
+ $response['success'] = FALSE;
+ $response['reason'] = $e->getMessage();
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: AutoRecurringCharge()
+
+ Handles the normal recurring charge, after the first one.
+
+ Parameters:
+ $order_id - An INT with the internal order id.
+ $gateway - An array of the gateway information.
+ $params
+ */
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $order_id, $params['api_customer_reference'], $params['amount']);
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: UpdateRecurring()
+
+ Updates the customer information for a subscription.
+ Not all gateways support this.
+
+ Parameters:
+ $gateway - An array of the the gateway's information.
+ $subscription - An array of the subscription information.
+ $customer - An array with the customer's information.
+ $params - Extra information.
+ */
+ public function UpdateRecurring($gateway, $subscription, $customer, $params)
+ {
+ /*
+ If nothing needs to be done at the gateway,
+ then simply return TRUE here. At this point
+ the user information will be already be updated
+ in OpenGateway.
+ */
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Method: CancelRecurring()
+
+ Cancels a recurring subscription at the gateway. Many gateways
+ don't support this ability.
+
+ Parameters:
+ $subscription - An array of subscription information.
+ */
+ public function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/payment/twocheckout.php b/app/modules/billing/libraries/payment/twocheckout.php
new file mode 100644
index 00000000..9bc11878
--- /dev/null
+++ b/app/modules/billing/libraries/payment/twocheckout.php
@@ -0,0 +1,953 @@
+settings = $this->Settings();
+
+ $this->CI =& get_instance();
+ }
+
+ //--------------------------------------------------------------------
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = '2Checkout';
+ $settings['class_name'] = 'twocheckout';
+ $settings['external'] = TRUE;
+ $settings['no_credit_card'] = TRUE;
+ $settings['description'] = '2Checkout is a simple 3rd-party PayPal alternative for international merchants. After account creation, you must setup your Notifications in your 2CO control panel. More information is available at in the support area.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$49';
+ $settings['monthly_fee'] = 'n/a';
+ $settings['transaction_fee'] = '5.5% + $0.45';
+ $settings['purchase_link'] = 'http://www.2checkout.com/';
+ $settings['allows_updates'] = 0;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 1;
+ $settings['requires_customer_ip'] = 0;
+ $settings['required_fields'] = array(
+ 'enabled',
+ 'mode',
+ 'username',
+ 'password',
+ 'merchant_id',
+ 'currency'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'test' => 'Test Mode'
+ )
+ ),
+ 'username' => array(
+ 'text' => 'API Username',
+ 'type' => 'text'
+ ),
+
+ 'password' => array(
+ 'text' => 'API Password',
+ 'type' => 'password'
+ ),
+ 'merchant_id' => array(
+ 'text' => 'Merchant ID',
+ 'type' => 'text'
+ ),
+ 'currency' => array(
+ 'text' => 'Currency',
+ 'type' => 'select',
+ 'options' => array(
+ 'USD' => 'US Dollar',
+ 'ARP' => 'Argentino Peso',
+ 'AUD' => 'Australian Dollar',
+ 'BRL' => 'Brazilian Real',
+ 'CAD' => 'Canadian Dollar',
+ 'DKK' => 'Danish Kroner',
+ 'EUR' => 'Euro',
+ 'GBP' => 'GBP Sterlings',
+ 'HKD' => 'Hong Kong Dollar',
+ 'INR' => 'Indian Rupee',
+ 'JPY' => 'Japanese Yen',
+ 'MXN' => 'Mexican Peso',
+ 'NAX' => 'New Zealand Dollar',
+ 'NOK' => 'Norwegian Kroner',
+ 'ZAL' => 'South African Rand',
+ 'SEK' => 'Swedish Kroner',
+ 'CHF' => 'Swiss Franc'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Verifies that our connection is working as expected by returning the
+ * detailed company info for the user.
+ *
+ * Also verifies that json is available on the server.
+ */
+ public function TestConnection($gateway)
+ {
+ if (!function_exists('json_encode'))
+ {
+ return false;
+ }
+
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Charging with 2CO does NOT require the product to be setup within
+ * the 2CO backend.
+ */
+ public function Charge($order_id, $gateway, $customer, $amount, $credit_card, $return_url, $cancel_url)
+ {
+ $this->CI->load->helper('url');
+ $this->CI->load->model('billing/charge_data_model');
+
+ // Return redirect information
+ $url = site_url('callback/twocheckout/form_redirect/'. $order_id);
+
+ // save return redirect URL
+ $this->CI->charge_data_model->Save($order_id, 'return_url', $return_url);
+
+ $response_array = array(
+ 'not_completed' => TRUE, // don't mark charge as complete
+ 'redirect' => $url, // redirect the user to this address
+ 'charge_id' => $order_id
+ );
+ $response = $this->CI->response->TransactionResponse(100, $response_array);
+
+ if ($this->debug)
+ {
+ echo 'Charge TransactionResponse
';
+ print_r($response);
+ die();
+ }
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Recur - called when an initial Recur charge comes through to
+ * to create a subscription.
+ *
+ */
+ public function Recur($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences, $return_url, $cancel_url)
+ {
+ $this->CI->load->helper('url');
+ $this->CI->load->model('billing/charge_data_model');
+
+ /*
+ First thing, we need to verify that the product exists at 2CO
+ or we need to create it. In this case, the product is a recurring
+ plan with a certain name and price
+ */
+
+ // Get our subscription details
+ $subscription = $this->CI->recurring_model->GetRecurring($subscription_id);
+
+ // Are we dealing with a plan or a one-time?
+ $plan_name = !empty($subscription['plan']['name']) ? $subscription['plan']['name'] : 'One Time Recurring Plan';
+
+ $plan_id = !empty($subscription['plan']['id']) ? $subscription['plan']['id'] : $subscription_id;
+
+ // Verify or create product
+ if (!$product_id = $this->api_product_exists($plan_name, $amount, $gateway))
+ {
+ $product_id = $this->api_create_product($plan_name, $plan_id, $amount, $start_date, $end_date, $interval, $gateway);
+ }
+
+ /*
+ Save the data for use within the redirect
+ */
+ $this->CI->charge_data_model->Save('r'.$subscription_id, 'product_id', $product_id);
+
+ // save the initial charge amount (it may be different, so we treat it as a separate first charge)
+ $this->CI->charge_data_model->Save('r' . $subscription_id, 'first_charge', $amount);
+
+ // save return redirect URL
+ $this->CI->charge_data_model->Save('r'.$subscription_id, 'return_url', $return_url);
+
+ // create the order if we need to
+ if ($charge_today === TRUE) {
+ $this->CI->load->model('billing/charge_model');
+
+ $customer_id = $customer['id'];
+
+ $order_id = $this->CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, array(), $subscription_id, $customer_id);
+
+ // the order will be set as failed but the callback_order_created_recur function will set it OK
+ }
+
+ /*
+ Since the actual recording of the success of the payment is handled
+ in the 'order_created' callback, we simply issue a redirect to take
+ the user to the 2CO checkout form.
+ */
+ $response_array = array(
+ 'not_completed' => TRUE, // don't mark charge as complete
+ 'redirect' => $url = site_url('callback/twocheckout/form_redirect_recur/'. $subscription_id), // redirect the user to this address
+ 'subscription_id' => $subscription_id
+ );
+
+ if (isset($order_id)) {
+ $response_array['charge_id'] = $order_id;
+ }
+
+ $response = $this->CI->response->TransactionResponse(100, $response_array);
+
+ return $response;
+ }
+
+ //--------------------------------------------------------------------
+
+ public function CancelRecurring($subscription, $gateway)
+ {
+ // First, we need to get the details of the sale so we have the line_item_id
+ $sale_details = $this->Process('sales/detail_sale', array('invoice_id' => $subscription['api_payment_reference']), $gateway);
+
+ if (!$sale_details or !isset($sale_details->lineitem_id))
+ {
+ return FALSE;
+ }
+
+ // Now try to cancel
+
+ $response = $this->Process('sales/stop_lineitem_recurring', array('lineitem_id' => $sale_details->lineitem_id), $gateway);
+
+ if ($response->response_code == 'OK') {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ public function UpdateRecurring($gateway, $subscription, $customer, $params)
+ {
+ return FALSE;
+ }
+
+ //--------------------------------------------------------------------
+
+ public function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $params);
+ }
+
+ //--------------------------------------------------------------------
+
+ public function ChargeRecurring($gateway, $params)
+ {
+ // We'll say that the charge went through, because as soon as it doesn't go through,
+ // this subscription is killed via the recurring_installment_stops
+
+ return array('success' => TRUE);
+ }
+
+ //--------------------------------------------------------------------
+
+ public function Refund ($gateway, $charge, $authorization)
+ {
+ $data = array(
+ 'invoice_id' => $authorization->tran_id,
+ 'amount' => $charge['amount'],
+ 'currency' => 'vendor',
+ 'category' => '5',
+ 'comment' => 'Issued by Vendor.'
+ );
+
+ $response = $this->Process('sales/refund_invoice', $data, $gateway);
+
+ if ($response->response_code == 'OK') {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Retrieves the charge id from the parameters passed in. This is used
+ * by the callback controller to retrieve the order information.
+ *
+ * @param array $params An array of $_POST and $_GET params passed by the gateway
+ * @return int $charge_id The id of the charge to look up.
+ */
+ public function GetChargeId($params)
+ {
+ if (isset($params['vendor_order_id']))
+ {
+ return $params['vendor_order_id'];
+ }
+ elseif (isset($params['merchant_order_id'])) {
+ return $params['merchant_order_id'];
+ }
+
+ return 0;
+ }
+
+ //--------------------------------------------------------------------
+
+ // Simply states whether this call is a recurring call or not
+ // by searching in the parameters passed from 2CO.
+ public function is_recurring($params)
+ {
+ if (isset($params['recurring']) && $params['recurring'] == '1')
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ //--------------------------------------------------------------------
+
+
+ //--------------------------------------------------------------------
+ // !API CALLS
+ //--------------------------------------------------------------------
+
+ public function api_product_exists($product_name, $amount, $gateway)
+ {
+ $response = $this->Process('products/list_products', array('product_name' => $product_name), $gateway);
+
+ if (!isset($response->products) or empty($response->products)) {
+ return false;
+ }
+
+ foreach ($response->products as $product)
+ {
+ if ($product->name == $product_name && $product->price == $amount)
+ {
+ return $product->assigned_product_id;
+ }
+ }
+
+ return false;
+ }
+
+ //--------------------------------------------------------------------
+
+ public function api_create_product($plan_name, $plan_id, $amount, $start_date, $end_date, $interval, $gateway)
+ {
+ $data = array(
+ 'name' => $plan_name,
+ 'price' => $amount,
+ 'vendor_product_id' => $plan_id,
+ 'tangible' => 0,
+ 'recurring' => 1,
+ 'recurrence' => (int)($interval/7) .' Week',
+ 'duration' => (int)((strtotime($end_date) - strtotime($start_date)) / 604800) .' Week',
+ 'commission' => 0
+ );
+
+ $response = $this->Process('products/create_product', $data, $gateway);
+
+ if ($response->response_code = 'OK')
+ {
+ return $response->assigned_product_id;
+ }
+
+ return false;
+ }
+
+ //--------------------------------------------------------------------
+
+
+ //--------------------------------------------------------------------
+ // !CALLBACKS
+ //--------------------------------------------------------------------
+
+ /**
+ * Used by the Charge method to redirect the user to the 2CO sales page.
+ */
+ public function Callback_form_redirect($gateway, $charge, $params)
+ {
+ $this->CI->load->helper('url');
+
+ // Build the form info to send to 2CO
+ $post = array(
+ 'sid' => $gateway['merchant_id'],
+ 'total' => $charge['amount'],
+ 'cart_order_id' => 'Invoice: '. $charge['id'],
+ 'id_type' => '1'
+ );
+
+ // product info
+ //$post['c_prod'] = $charge['type'];
+ //$post['c_name'] = $charge['type'];
+ //$post['c_description'] = $charge['type'];
+ //$post['c_price'] = $charge['amount'];
+
+ // Test/dev mode?
+ if ($gateway['mode'] != 'live')
+ {
+ $post['demo'] = 'Y';
+ }
+
+ $post['fixed'] = 'Y';
+ $post['x_receipt_link_url'] = site_url('callback/twocheckout/redirect/' . $charge['id']);
+ $post['merchant_order_id'] = $charge['id'];
+ $post['pay_method'] = 'CC';
+ $post['skip_landing'] = 'Y';
+
+ // Billing info
+ if (isset($charge['customer']['first_name']))
+ {
+ $post['card_holder_name'] = $charge['customer']['first_name'] .' '. $charge['customer']['last_name'];
+ }
+ if (isset($charge['customer']['address_1']) and !empty($charge['customer']['address_1']))
+ {
+ $post['street_address'] = $charge['customer']['address_1'];
+ $post['street_address2'] = $charge['customer']['address_2'];
+ $post['city'] = $charge['customer']['city'];
+ $post['state'] = $charge['customer']['state'];
+ $post['zip'] = $charge['customer']['postal_code'];
+ $post['country'] = $charge['customer']['country'];
+ }
+ $post['email'] = $charge['customer']['email'];
+ $post['phone'] = $charge['customer']['phone'];
+
+ $data = '';
+ foreach ($post as $key => $value)
+ {
+ $data .= "&$key=$value";
+ }
+
+ //redirect('http://developers.2checkout.com/return_script/?'. trim($data, '& '));
+ redirect('https://www.2checkout.com/checkout/purchase?'. trim($data, '& '));
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Used by the Recur method to redirect the user to the 2CO sales page.
+ */
+ public function Callback_form_redirect_recur($gateway, $charge, $params)
+ {
+ $this->CI->load->model('billing/charge_data_model');
+ $this->CI->load->helper('url');
+
+ $charge_data = $this->CI->charge_data_model->Get('r'. $charge['id']);
+
+ // Build our form info for 2CO
+ $post = array(
+ 'sid' => $gateway['merchant_id'],
+ 'product_id' => $charge_data['product_id'],
+ 'quantity' => '1',
+ 'demo' => $gateway['mode'] != 'live' ? 'Y' : 'n',
+ 'fixed' => 'Y',
+ 'merchant_order_id' => $charge['id'],
+ 'pay_method' => 'CC',
+ 'skip_landing' => 'Y',
+ 'x_receipt_link_url' => site_url('callback/twocheckout/redirect_recur/' . $charge['id'])
+ );
+
+ // Billing info
+ if (isset($charge['customer']['first_name']))
+ {
+ $post['card_holder_name'] = $charge['customer']['first_name'] .' '. $charge['customer']['last_name'];
+ }
+ if (isset($charge['customer']['address_1']) and !empty($charge['customer']['address_1']))
+ {
+ $post['street_address'] = $charge['customer']['address_1'];
+ $post['street_address2'] = $charge['customer']['address_2'];
+ $post['city'] = $charge['customer']['city'];
+ $post['state'] = $charge['customer']['state'];
+ $post['zip'] = $charge['customer']['postal_code'];
+ $post['country'] = $charge['customer']['country'];
+ }
+
+ $post['email'] = $charge['customer']['email'];
+ $post['phone'] = $charge['customer']['phone'];
+
+ $data = '';
+ foreach ($post as $key => $value)
+ {
+ $data .= "&$key=$value";
+ }
+
+ //redirect('http://developers.2checkout.com/return_script/?'. trim($data, '& '));
+ redirect('https://www.2checkout.com/checkout/purchase?'. trim($data, '& '));
+ }
+
+ //--------------------------------------------------------------------
+
+ public function Callback_redirect ($gateway, $charge, $params) {
+ $this->CI->load->model('billing/charge_data_model');
+ $this->CI->load->helper('url');
+
+ $charge_data = $this->CI->charge_data_model->Get($charge['id']);
+
+ $this->CI->load->model('billing/order_authorization_model');
+ $this->CI->order_authorization_model->SaveAuthorization($charge['id'], $charge['id']);
+
+ $this->CI->charge_model->SetStatus($charge['id'], 1);
+
+ return redirect($charge_data['return_url']);
+ }
+
+ public function Callback_redirect_recur ($gateway, $charge, $params) {
+ $this->CI->load->model('billing/charge_data_model');
+ $this->CI->load->helper('url');
+
+ $charge_data = $this->CI->charge_data_model->Get('r'. $charge['id']);
+
+ return redirect($charge_data['return_url']);
+ }
+
+ /**
+ * Called when a user succesffully creates either a Charge or a Recurring charge.
+ * Saves the information to the datase and lets us know it's a successfull charge.
+ */
+ public function Callback_order_created($gateway, $charge, $params)
+ {
+ // Is this a recurring order?
+ if ($params['recurring'] == '1') {
+ $this->Callback_recurring_order_created($gateway, $charge, $params);
+ }
+ else {
+ // we do this logic in Callback_redirect now
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Called by 2CO whenever a recurring order is created.
+ */
+ public function Callback_recurring_order_created($gateway, $subscription, $params)
+ {
+ $this->CI->load->model('billing/recurring_model');
+
+ $this->CI->load->model('billing/charge_data_model');
+ $data = $this->CI->charge_data_model->Get('r' . $subscription['id']);
+
+ // do we have a first charge to process
+ $result = $this->CI->db->where('subscription_id',$subscription['id'])
+ ->from('orders')
+ ->get();
+
+ if ($result->num_rows() > 0) {
+ $order = $result->row_array();
+
+ $this->CI->load->model('billing/charge_model');
+ $this->CI->load->model('billing/order_authorization_model');
+
+ // we may not have the transaction ID if it's Pending
+ $this->CI->order_authorization_model->SaveAuthorization($order['order_id'], $params['sale_id']);
+
+ $this->CI->charge_model->SetStatus($order['order_id'], 1);
+ }
+
+ $order_id = (isset($order_id)) ? $order_id : FALSE;
+
+ $this->CI->recurring_model->SetActive($subscription['id']);
+
+ // hook
+ $this->CI->load->library('app_hooks');
+ $this->CI->app_hooks->data('subscription', $subscription['id']);
+ $this->CI->app_hooks->trigger('subscription_new', $subscription['id']);
+
+ // trip a recurring charge?
+ if ($order_id) {
+ // hook
+ $this->CI->app_hooks->data('invoice', $order_id);
+ $this->CI->app_hooks->trigger('subscription_charge', $order_id, $subscription['id']);
+ }
+
+ $this->CI->app_hooks->reset();
+
+ // Save our Invoice ID so we can get details of line_items later.
+ $this->CI->recurring_model->SaveApiPaymentReference($subscription['id'], $params['invoice_id']);
+
+ // (don't) Delete our temp data!
+ // we used to delete this and it screwed the post-order redirects
+ //$this->CI->charge_data_model->delete('r'. $subscription['id']);
+ die();
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Called by 2CO on a successfull installment charge.
+
+ This method saves the order authorization information.
+ */
+ public function Callback_recurring_installment_success($gateway, $subscription, $params)
+ {
+ // OG will automatically create successful orders, so there's no need to duplicate this here
+
+ /*
+ $this->CI->load->model('billing/recurring_model');
+ $this->CI->load->model('billing/charge_model');
+
+ // Record a new charge for this one.
+ $customer_id = (isset($subscription['customer']['id'])) ? $subscription['customer']['id'] : FALSE;
+ $order_id = $this->CI->charge_model->CreateNewOrder($gateway['gateway_id'], $subscription['amount'], array(), $subscription['id'], $customer_id);
+
+ // hook
+ $this->CI->load->library('app_hooks');
+ $this->CI->app_hooks->data('subscription', $subscription['id']);
+ $this->CI->app_hooks->data('invoice', $order_id);
+ $this->CI->app_hooks->trigger('subscription_charge', $order_id, $subscription['id']);
+
+ $this->CI->app_hooks->reset();
+
+ // Save our Invoice ID so we can get details of line_items later.
+ $this->CI->recurring_model->SaveApiPaymentReference($subscription['id'], $params['invoice_id']);*/
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Called by 2CO on a failure to bill a recurring payment.
+
+ This simply records the failure in the database.
+ */
+ public function Callback_recurring_installment_fail($gateway, $subscription, $params)
+ {
+ $this->CI->load->model('billing/recurring_model');
+
+ $this->CI->recurring_model->AddFailure($subscription['id'], 1);
+ }
+
+ //--------------------------------------------------------------------
+
+ /*
+ Called by 2CO when a recurring plan has been stopped.
+ */
+ public function Callback_recurring_installment_stopped($gateway, $subscription, $params)
+ {
+ // Mark the subscription as inactive
+ $this->CI->load->model('billing/recurring_model');
+
+ $this->CI->recurring_model->MakeInactive($subscription['id']);
+
+ $this->CI->recurring_model->CancelRecurring($subscription['id'], TRUE);
+
+ $this->CI->load->library('app_hooks');
+ $this->CI->app_hooks->data('subscription', $subscription['id']);
+ $this->CI->app_hooks->trigger('subscription_expire', $subscription['id']);
+
+ $this->CI->app_hooks->reset();
+ }
+
+ //--------------------------------------------------------------------
+
+ //--------------------------------------------------------------------
+ // !PROCESSORS
+ //--------------------------------------------------------------------
+
+ public function Process($url_suffix, $data, $gateway)
+ {
+ if(!is_array($data))
+ {
+ $resp = $this->return_response(array('Error' => 'Value passed in was not an array of at least one key/value pair.'));
+ }
+ else
+ {
+ if (strpos($url_suffix, 'http') !== false)
+ {
+ $url = $url_suffix;
+ }
+ else
+ {
+ $url = $this->base_url . $url_suffix;
+ }
+
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array("Accept: {$this->accept}/{$this->format}"));
+ curl_setopt($ch, CURLINFO_HEADER_OUT, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ //curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
+ curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+ curl_setopt($ch, CURLOPT_USERPWD, "{$gateway['username']}:{$gateway['password']}");
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); // Verify it belongs to the server.
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // Check common exists and matches the server host name
+
+ if(count($data) > 0) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+ }
+
+ $resp = curl_exec($ch);
+
+ if ($this->debug)
+ {
+ echo 'CURL Response
';
+ print_r(curl_getinfo($ch));
+ echo 'CURL Error = '. curl_error($ch) ."
";
+ }
+ curl_close($ch);
+ }
+
+ if ($this->debug)
+ {
+ echo 'URL = '. $url .'
';
+ echo 'Process Results
';
+ var_dump($resp);
+ die();
+ }
+
+ return json_decode($this->return_response($resp));
+ }
+
+ //--------------------------------------------------------------------
+
+ //--------------------------------------------------------------------
+ // ! PRIVATE METHODS
+ //--------------------------------------------------------------------
+
+ /**
+ * Formats the return response based upon the content types.
+ *
+ * @param string $contents An array where keys are nodes and values are the node data
+ * @return array
+ */
+ public function return_response($contents)
+ {
+ switch($this->format) {
+ case 'xml':
+ if(preg_match('//', $contents)) {
+ return $contents;
+ } else {
+ $xml = new XmlConstruct('response');
+ $xml->fromArray($contents);
+ return $xml->getDocument();
+ return $xml->output();
+ }
+ break;
+ case 'json':
+ if(preg_match('/response :/', $contents)) {
+ return $contents;
+ } else {
+ $jsonData = json_encode($contents);
+ return json_decode($jsonData);
+ }
+ break;
+ case 'html':
+ if(preg_match('/\response_code\<\/dt\>/', $contents)) {
+ return $contents;
+ } else {
+ $htmlOut = '';
+ foreach($contents as $key => $val) {
+ $htmlOut .= "$key- $val
\n";
+ }
+ return $htmlOut;
+ }
+ break;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Returns the proper url for the remote gateway.
+ *
+ * Note that $mode param defaults to false, which will
+ * return the token payments url. If $mode is 'rebill',
+ * then it will return the rebill url.
+ */
+ private function GetAPIUrl ($gateway, $mode = FALSE) {
+ if ($mode == FALSE) {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['url_dev'];
+ break;
+ }
+ }
+ elseif ($mode == 'rebill') {
+ // Get the proper URL
+ switch($gateway['mode'])
+ {
+ case 'live':
+ $post_url = $gateway['arb_url_live'];
+ break;
+ case 'test':
+ $post_url = $gateway['arb_url_test'];
+ break;
+ case 'dev':
+ $post_url = $gateway['arb_url_dev'];
+ break;
+ }
+ }
+
+ return $post_url;
+ }
+
+ //--------------------------------------------------------------------
+
+ private function xml2array($xml) {
+ $xmlary = array();
+
+ $reels = '/<(\w+)\s*([^\/>]*)\s*(?:\/>|>(.*)<\/\s*\\1\s*>)/s';
+ $reattrs = '/(\w+)=(?:"|\')([^"\']*)(:?"|\')/';
+
+ preg_match_all($reels, $xml, $elements);
+
+ foreach ($elements[1] as $ie => $xx) {
+ $xmlary[$ie]["name"] = $elements[1][$ie];
+
+ if ($attributes = trim($elements[2][$ie])) {
+ preg_match_all($reattrs, $attributes, $att);
+ foreach ($att[1] as $ia => $xx)
+ $xmlary[$ie]["attributes"][$att[1][$ia]] = $att[2][$ia];
+ }
+
+ $cdend = strpos($elements[3][$ie], "<");
+ if ($cdend > 0) {
+ $xmlary[$ie]["text"] = substr($elements[3][$ie], 0, $cdend - 1);
+ }
+
+ if (preg_match($reels, $elements[3][$ie]))
+ $xmlary[$ie]["elements"] = $his->xml2array($elements[3][$ie]);
+ else if ($elements[3][$ie]) {
+ $xmlary[$ie]["text"] = $elements[3][$ie];
+ }
+ }
+
+ return $xmlary;
+ }
+}
+
+// End twocheckout class
+
+//--------------------------------------------------------------------
+
+/**
+ * XMLParser Class
+ *
+ * This class loads an XML document into a SimpleXMLElement that can
+ * be processed by the calling application. This accepts xml strings,
+ * files, and DOM objects. It can also perform the reverse, converting
+ * an SimpleXMLElement back into a string, file, or DOM object.
+ *
+ * I am not sure who the original author of this class is as it was
+ * never documented. Henceforth, I am reliquishing ownership of this
+ * class.
+ */
+class XmlConstruct extends XMLWriter
+{
+ private $formal = false;
+ /**
+ * Constructor.
+ * @param string $prm_rootElementName A root element's name of a current xml document
+ * @param string $prm_xsltFilePath Path of a XSLT file.
+ * @access public
+ * @param null
+ */
+ public function __construct($prm_rootElementName, $formal=false, $prm_xsltFilePath='') {
+ $this->formal = $formal;
+ $this->openMemory();
+ $this->setIndent(true);
+ $this->setIndentString(' ');
+ if($this->formal) {
+ $this->startDocument('1.0', 'UTF-8');
+ }
+
+ if($prm_xsltFilePath) {
+ $this->writePi('xml-stylesheet', 'type="text/xsl" href="'.$prm_xsltFilePath.'"');
+ }
+
+ $this->startElement($prm_rootElementName);
+ }
+
+ /**
+ * Set an element with a text to a current xml document.
+ * @access public
+ * @param string $prm_elementName An element's name
+ * @param string $prm_ElementText An element's text
+ * @return null
+ */
+ public function setElement($prm_elementName, $prm_ElementText) {
+ $this->startElement($prm_elementName);
+ $this->text($prm_ElementText);
+ $this->endElement();
+ }
+
+ /**
+ * Construct elements and texts from an array.
+ * The array should contain an attribute's name in index part
+ * and a attribute's text in value part.
+ * @access public
+ * @param array $prm_array Contains attributes and texts
+ * @return null
+ */
+ public function fromArray($prm_array) {
+ if(is_array($prm_array)) {
+ foreach ($prm_array as $index => $element) {
+ if(is_array($element)) {
+ $this->startElement($index);
+ $this->fromArray($element);
+ $this->endElement();
+ }
+ else
+ $this->setElement($index, $element);
+
+ }
+ }
+ }
+
+ /**
+ * Return the content of a current xml document.
+ * @access public
+ * @param null
+ * @return string Xml document
+ */
+ public function getDocument() {
+ $this->endElement();
+ if($this->formal) {
+ $this->endDocument();
+ }
+ return $this->outputMemory();
+ }
+
+}
diff --git a/app/modules/billing/libraries/payment/wirecard.php b/app/modules/billing/libraries/payment/wirecard.php
new file mode 100644
index 00000000..17cc2074
--- /dev/null
+++ b/app/modules/billing/libraries/payment/wirecard.php
@@ -0,0 +1,422 @@
+settings = $this->Settings();
+ }
+
+ function Settings()
+ {
+ $settings = array();
+
+ $settings['name'] = 'Wirecard';
+ $settings['class_name'] = 'wirecard';
+ $settings['external'] = FALSE;
+ $settings['no_credit_card'] = FALSE;
+ $settings['description'] = 'Wirecard is a premium provider of online credit card processing services available for international merchants.';
+ $settings['is_preferred'] = 1;
+ $settings['setup_fee'] = '$33';
+ $settings['monthly_fee'] = '$19';
+ $settings['transaction_fee'] = '$0.23';
+ $settings['purchase_link'] = 'http://www.wirecard.com/products/payment/credit-card-processing.html';
+ $settings['allows_updates'] = 1;
+ $settings['allows_refunds'] = 1;
+ $settings['requires_customer_information'] = 1;
+ $settings['requires_customer_ip'] = 1;
+ $settings['required_fields'] = array(
+ 'enabled',
+ 'mode',
+ 'gateway_url',
+ 'username',
+ 'password',
+ 'businesscasesignature',
+ 'currency',
+ 'accept_visa',
+ 'accept_mc',
+ 'accept_discover',
+ 'accept_dc',
+ 'accept_amex'
+ );
+
+ $settings['field_details'] = array(
+ 'enabled' => array(
+ 'text' => 'Enable this gateway?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Enabled',
+ '0' => 'Disabled')
+ ),
+ 'mode' => array(
+ 'text' => 'Mode',
+ 'type' => 'select',
+ 'options' => array(
+ 'live' => 'Live Mode',
+ 'demo' => 'Test Mode'
+ )
+ ),
+ 'gateway_url' => array(
+ 'text' => 'Gateway URL',
+ 'type' => 'text'
+ ),
+ 'username' => array(
+ 'text' => 'Username',
+ 'type' => 'text'
+ ),
+ 'password' => array(
+ 'text' => 'Password',
+ 'type' => 'password'
+ ),
+ 'businesscasesignature' => array(
+ 'text' => 'Business Case Signature',
+ 'type' => 'text'
+ ),
+ 'currency' => array(
+ 'text' => 'Currency',
+ 'type' => 'select',
+ 'options' => array(
+ 'GBP' => 'GBP - Pound Sterling',
+ 'EUR' => 'EUR - Euro',
+ 'USD' => 'USD - US Dollar',
+ 'AUD' => 'AUD - Australian Dollar',
+ 'CAD' => 'CAD - Canadian Dollar',
+ 'CHF' => 'CHF - Swiss Franc',
+ 'DKK' => 'DKK - Danish Krone',
+ 'HKD' => 'HKD - Hong Kong Dollar',
+ 'IDR' => 'IDR - Rupiah',
+ 'ISK' => 'ISK - Icelandic Krona',
+ 'JPY' => 'JPY - Yen',
+ 'LUF' => 'LUF - Luxembourg Franc',
+ 'NOK' => 'NOK - Norwegian Krone',
+ 'NZD' => 'NZD - New Zealand Dollar',
+ 'SEK' => 'SEK - Swedish Krona',
+ 'SGD' => 'SGD - Singapore Dollar',
+ 'TRL' => 'TRL - Turkish Lira'
+ )
+ ),
+ 'accept_visa' => array(
+ 'text' => 'Accept VISA?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_mc' => array(
+ 'text' => 'Accept MasterCard?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_discover' => array(
+ 'text' => 'Accept Discover?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_dc' => array(
+ 'text' => 'Accept Diner\'s Club?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ ),
+ 'accept_amex' => array(
+ 'text' => 'Accept American Express?',
+ 'type' => 'radio',
+ 'options' => array(
+ '1' => 'Yes',
+ '0' => 'No'
+ )
+ )
+ );
+
+ return $settings;
+ }
+
+ function TestConnection($gateway)
+ {
+ // There's no way to test the connection at this point
+ return TRUE;
+ }
+
+ function Charge($order_id, $gateway, $customer, $amount, $credit_card)
+ {
+ $CI =& get_instance();
+
+ $transaction = $this->TransactionArray($order_id, $gateway, $customer, $amount, $credit_card);
+
+ $response = $this->Process($gateway, $transaction);
+
+ $result = $response['W_RESPONSE']['W_JOB']['FNC_CC_PURCHASE']['CC_TRANSACTION']['PROCESSING_STATUS'];
+
+ if ($result['FunctionResult'] == 'ACK' or $result['FunctionResult'] == 'PENDING') {
+ $CI->load->model('billing/order_authorization_model');
+ $CI->order_authorization_model->SaveAuthorization($order_id, $result['GuWID']);
+
+ $CI->load->model('billing/charge_model');
+ $CI->charge_model->SetStatus($order_id, 1);
+
+ $response_array = array('charge_id' => $order_id);
+ $response = $CI->response->TransactionResponse(1, $response_array);
+ } else {
+ $response_array = array('reason' => $result['ERROR']['Message'] . ' (' . $result['ERROR']['Number'] . ')');
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+
+ return $response;
+ }
+
+ function Recur ($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences = FALSE)
+ {
+ $CI =& get_instance();
+
+ $CI->load->model('billing/order_authorization_model');
+
+ $successful_recur = FALSE;
+
+ // if a payment is to be made today, process it.
+ if ($charge_today === TRUE) {
+ // Create an order for today's payment
+ $CI->load->model('billing/charge_model');
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer['ip_address']);
+
+ $transaction = $this->TransactionArray($order_id, $gateway, $customer, $amount, $credit_card, 'Initial');
+
+ $response = $this->Process($gateway, $transaction);
+
+ $result = $response['W_RESPONSE']['W_JOB']['FNC_CC_PURCHASE']['CC_TRANSACTION']['PROCESSING_STATUS'];
+
+ if ($result['FunctionResult'] == 'ACK' or $result['FunctionResult'] == 'PENDING') {
+ $CI->charge_model->SetStatus($order_id, 1);
+ $CI->order_authorization_model->SaveAuthorization($order_id, $result['GuWID']);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ $successful_recur = TRUE;
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+
+ $response_array = array('reason' => $result['ERROR']['Message'] . ' (' . $result['ERROR']['Number'] . ')');
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ } else {
+ // we need to process an initial AUTHENTICATE transaction in order to send REPEATs later
+
+ // we'll use a $0 order not linked to any customer
+ $CI->load->model('billing/charge_model');
+ $order_id = $CI->charge_model->CreateNewOrder($gateway['gateway_id'], '0.00', $credit_card, $subscription_id);
+
+ $transaction = $this->TransactionArray($order_id, $gateway, $customer, $amount, $credit_card, 'Initial', 'FNC_CC_AUTHORIZATION');
+
+ $response = $this->Process($gateway, $transaction);
+
+ $result = $response['W_RESPONSE']['W_JOB']['FNC_CC_AUTHORIZATION']['CC_TRANSACTION']['PROCESSING_STATUS'];
+
+ if ($result['FunctionResult'] == 'ACK' or $result['FunctionResult'] == 'PENDING') {
+ $CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('recurring_id' => $subscription_id);
+ $response = $CI->response->TransactionResponse(100, $response_array);
+ $successful_recur = TRUE;
+ } else {
+ // Make the subscription inactive
+ $CI->recurring_model->MakeInactive($subscription_id);
+
+ $response_array = array('reason' => $result['ERROR']['Message'] . ' (' . $result['ERROR']['Number'] . ')');
+ $response = $CI->response->TransactionResponse(2, $response_array);
+ }
+ }
+
+ if ($successful_recur == TRUE) {
+ // let's save the transaction details for future REPEATs
+ $CI->recurring_model->SaveApiPaymentReference($subscription_id, $result['GuWID']);
+ }
+
+ return $response;
+ }
+
+ function Refund ($gateway, $charge, $authorization)
+ {
+ $CI =& get_instance();
+
+ $transaction = array(
+ 'WIRECARD_BXML' => array(
+ 'W_REQUEST' => array(
+ 'W_JOB' => array(
+ 'BusinessCaseSignature' => $gateway['businesscasesignature'],
+ 'FNC_CC_BOOKBACK' => array(
+ 'CC_TRANSACTION' => array(
+ 'TransactionID' => $charge['id'],
+ 'GuWID' => $authorization->tran_id,
+ 'Amount' => (int)($charge['amount'] * 100),
+ 'Currency' => $gateway['currency']
+ )
+ )
+ )
+ )
+ )
+ );
+
+ $response = $this->Process($gateway, $transaction);
+
+ if (isset($response['W_RESPONSE']['W_JOB']['FNC_CC_BOOKBACK']['CC_TRANSACTION']['PROCESSING_STATUS']['FunctionResult']) and $response['W_RESPONSE']['W_JOB']['FNC_CC_BOOKBACK']['CC_TRANSACTION']['PROCESSING_STATUS']['FunctionResult'] == 'ACK') {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ function CancelRecurring($subscription)
+ {
+ return TRUE;
+ }
+
+ function AutoRecurringCharge ($order_id, $gateway, $params) {
+ return $this->ChargeRecurring($gateway, $order_id, $params['api_payment_reference'], $params['amount']);
+ }
+
+ function ChargeRecurring($gateway, $order_id, $GuWID, $amount)
+ {
+ $CI =& get_instance();
+
+ $transaction = array(
+ 'WIRECARD_BXML' => array(
+ 'W_REQUEST' => array(
+ 'W_JOB' => array(
+ 'BusinessCaseSignature' => $gateway['businesscasesignature'],
+ 'FNC_CC_PURCHASE' => array(
+ 'CC_TRANSACTION' => array(
+ 'TransactionID' => $order_id,
+ 'GuWID' => $GuWID,
+ 'Amount' => (int)($amount * 100),
+ 'Currency' => $gateway['currency'],
+ 'CountryCode' => $CI->config->item('locale'),
+ 'RECURRING_TRANSACTION' => array(
+ 'Type' => 'Repeated',
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ $response = $this->Process($gateway, $transaction);
+
+ $result = $response['W_RESPONSE']['W_JOB']['FNC_CC_PURCHASE']['CC_TRANSACTION']['PROCESSING_STATUS'];
+
+ if ($result['FunctionResult'] == 'ACK' or $result['FunctionResult'] == 'PENDING'){
+ $response = array(
+ 'success' => TRUE
+ );
+ } else {
+ $response = array();
+
+ $response['success'] = FALSE;
+ $response['reason'] = $response['reason'];
+ }
+
+ return $response;
+ }
+
+ function UpdateRecurring()
+ {
+ return TRUE;
+ }
+
+ function Process($gateway, $transaction_array)
+ {
+ $CI =& get_instance();
+
+ $CI->load->library('array_to_xml');
+
+ $transaction_xml = $CI->array_to_xml->ToXML($transaction_array);
+
+ // make unique adjustments
+ $transaction_xml = (string)$transaction_xml;
+ $transaction_xml = str_replace('','',$transaction_xml);
+ $transaction_xml = str_replace('','',$transaction_xml);
+ $transaction_xml = str_replace('','',$transaction_xml);
+ $transaction_xml = str_replace('','',$transaction_xml);
+ $transaction_xml = str_replace(' ','',$transaction_xml);
+
+ $post_url = $gateway['gateway_url'];
+
+ $request = curl_init($post_url); // initiate curl object
+ curl_setopt($request, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml", "Authorization: Basic " . base64_encode($gateway['username'] . ':' . $gateway['password'])));
+ curl_setopt($request, CURLOPT_HEADER, 0); // set to 0 to eliminate header info from response
+ curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); // Returns response data instead of TRUE(1)
+ curl_setopt($request, CURLOPT_POSTFIELDS, $transaction_xml); // use HTTP POST to send form data
+ curl_setopt($request, CURLOPT_SSL_VERIFYPEER, FALSE); // uncomment this line if you get no gateway response.
+ $post_response = curl_exec($request); // execute curl post and store results in $post_response
+
+ curl_close ($request); // close curl object
+
+ $response = $CI->array_to_xml->ToArray($post_response);
+
+ return $response;
+ }
+
+ function TransactionArray ($order_id, $gateway, $customer, $amount, $credit_card, $type = 'Single', $node = 'FNC_CC_PURCHASE') {
+ $CI =& get_instance();
+
+ $transaction = array(
+ 'WIRECARD_BXML' => array(
+ 'W_REQUEST' => array(
+ 'W_JOB' => array(
+ 'BusinessCaseSignature' => $gateway['businesscasesignature'],
+ $node => array(
+ 'CC_TRANSACTION' => array(
+ 'TransactionID' => $order_id,
+ 'Amount' => (int)($amount * 100),
+ 'Currency' => $gateway['currency'],
+ 'CountryCode' => $CI->config->item('locale'),
+ 'RECURRING_TRANSACTION' => array(
+ 'Type' => $type,
+ ),
+ 'CREDIT_CARD_DATA' => array(
+ 'CreditCardNumber' => $credit_card['card_num'],
+ 'CVC2' => $credit_card['cvv'],
+ 'ExpirationYear' => $credit_card['exp_year'],
+ 'ExpirationMonth' => str_pad($credit_card['exp_month'], 2, "0", STR_PAD_LEFT),
+ 'CardHolderName' => isset($credit_card['name']) ? $credit_card['name'] : $credit_card['card_name']
+ ),
+ 'CONTACT_DATA' => array(
+ 'IPAddress' => isset($customer['ip_address']) ? $customer['ip_address'] : '0.0.0.0'
+ ),
+ 'CORPTRUSTCENTER_DATA' => array(
+ 'ADDRESS' => array(
+ 'FirstName' => $customer['first_name'],
+ 'LastName' => $customer['last_name'],
+ 'Address1' => $customer['address_1'],
+ 'Address2' => $customer['address_2'],
+ 'City' => $customer['city'],
+ 'State' => $customer['state'],
+ 'ZipCode' => $customer['postal_code'],
+ 'Country' => $customer['country'],
+ 'Email' => $customer['email']
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ // if this is outside Canada or USA, we don't need state
+ if ($customer['country'] != 'US' or $customer['country'] != 'CA') {
+ unset($transaction['WIRECARD_BXML']['W_REQUEST']['W_JOB'][$node]['CC_TRANSACTION']['CORPTRUSTCENTER_DATA']['ADDRESS']['State']);
+ }
+
+ return $transaction;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/paypal_fix.php b/app/modules/billing/libraries/paypal_fix.php
new file mode 100644
index 00000000..bcc45f83
--- /dev/null
+++ b/app/modules/billing/libraries/paypal_fix.php
@@ -0,0 +1,197 @@
+CI =& get_instance();
+ }
+
+ function hook_cron ()
+ {
+ cron_log('Beginning the PayPal charge date fixes.');
+
+ // only run once at 5am
+ if (date('H') != 5) {
+ cron_log('Cronjob should only run once between 5:03 and 5:11. Exiting (1).');
+
+ return TRUE;
+ }
+
+ if (date('i') < 3 or date('i') > 11) {
+ cron_log('Cronjob should only run once between 5:03 and 5:11. Exiting (2).');
+
+ return TRUE;
+ }
+
+ set_time_limit(0);
+
+ $this->CI->db->select('*');
+ $this->CI->db->from('subscriptions');
+ $this->CI->db->join('gateways','gateways.gateway_id = subscriptions.gateway_id','inner');
+ $this->CI->db->join('external_apis','external_apis.external_api_id = gateways.external_api_id','inner');
+ $this->CI->db->where('(`external_apis`.`name` = \'paypal\' or `external_apis`.`name` = \'paypal_standard\')',NULL,FALSE);
+ $this->CI->db->where('subscriptions.active','1');
+ $this->CI->db->where('subscriptions.next_charge <',date('Y-m-d', strtotime('now +4 days')));
+ $this->CI->db->where('subscriptions.next_charge >',date('Y-m-d', strtotime('tomorrow')));
+ $result = $this->CI->db->get();
+
+ $updated = 0;
+
+ if ($result->num_rows() > 0) {
+ foreach ($result->result_array() as $sub) {
+ $profile = $this->_get_profile($sub['gateway_id'], $sub['api_customer_reference']);
+
+ if (isset($profile['NEXTBILLINGDATE']) and !empty($profile['NEXTBILLINGDATE'])) {
+ $paypal_date = $profile['NEXTBILLINGDATE'];
+ $local_date = $sub['next_charge'];
+ $formatted_paypal_date = date('Y-m-d', strtotime($profile['NEXTBILLINGDATE']));
+
+ // update local
+ $this->CI->db->update('subscriptions', array('next_charge' => $formatted_paypal_date), array('subscription_id' => $sub['subscription_id']));
+
+ $updated++;
+ }
+ }
+ }
+
+ cron_log('Fixed PayPal subscription next_charge dates: ' . $updated);
+
+ return TRUE;
+ }
+
+ function _get_profile ($gateway_id, $profile_id)
+ {
+ if (isset($this->gateways[$gateway_id])) {
+ $gateway = $this->gateways[$gateway_id];
+ }
+ else {
+ $gateway = $this->_get_gateway_details($gateway_id);
+ $this->gateways[$gateway_id] = $gateway;
+ }
+
+ $post_url = $this->_get_api_url($gateway);
+
+ $post = array();
+ $post['version'] = '60';
+ $post['method'] = 'GetRecurringPaymentsProfileDetails';
+ $post['user'] = $gateway['user'];
+ $post['pwd'] = $gateway['pwd'];
+ $post['signature'] = $gateway['signature'];
+ $post['profileid'] = $profile_id;
+
+ $post_response = $this->_process($post_url, $post);
+
+ if ($post_response['ACK'] == 'Success') {
+ return $post_response;
+ } else {
+ return FALSE;
+ }
+ }
+
+ private function _get_gateway_details ($gateway_id) {
+ $this->CI->load->library('encrypt');
+
+ $this->CI->db->where('gateway_id',$gateway_id);
+ $result = $this->CI->db->get('gateway_params');
+
+ $data = array();
+ foreach ($result->result_array() as $item) {
+ $data[$item['field']] = $this->CI->encrypt->decode($item['value']);
+ }
+
+ $this->CI->db->where('gateway_id', $gateway_id);
+ $result = $this->CI->db->get('gateways');
+
+ $gateway = $result->row_array();
+
+ $this->CI->db->where('external_api_id', $gateway['external_api_id']);
+ $result = $this->CI->db->get('external_apis');
+
+ $external_api = $result->row_array();
+
+ foreach ($external_api as $item => $value) {
+ $data[$item] = $value;
+ }
+
+ return $data;
+ }
+
+ private function _get_api_url ($gateway) {
+ if ($gateway['mode'] == 'test') {
+ return $gateway['test_url'];
+ }
+ else {
+ return $gateway['prod_url'];
+ }
+ }
+
+ private function _process ($url, $post_data)
+ {
+ $data = '';
+
+ foreach ($post_data as $key => $value) {
+ if (!empty($value)) {
+ $data .= strtoupper($key) . '=' . urlencode(trim($value)) . '&';
+ }
+ }
+
+ // remove the extra ampersand
+ $data = substr($data, 0, strlen($data) - 1);
+
+ // setting the curl parameters.
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_VERBOSE, 1);
+
+ // turning off the server and peer verification(TrustManager Concept).
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
+
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_POST, 1);
+
+ // setting the nvpreq as POST FIELD to curl
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+
+ // getting response from server
+ $response = curl_exec($ch);
+
+ // Throw an error if we can't continue. Will help in debugging.
+ if (curl_error($ch)) {
+ show_error(curl_error($ch));
+ }
+
+ $response = $this->_response_to_array($response);
+
+ return $response;
+ }
+
+ private function _response_to_array($string)
+ {
+ $string = urldecode($string);
+ $pairs = explode('&', $string);
+ $values = array();
+
+ foreach($pairs as $pair)
+ {
+ list($key, $value) = explode('=', $pair);
+ $values[$key] = $value;
+ }
+
+ return $values;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/libraries/stripe/Stripe.php b/app/modules/billing/libraries/stripe/Stripe.php
new file mode 100644
index 00000000..c30e5deb
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe.php
@@ -0,0 +1,65 @@
+_apiKey = $apiKey;
+ }
+
+ public static function apiUrl($url='')
+ {
+ $apiBase = Stripe::$apiBase;
+ return "$apiBase$url";
+ }
+
+ public static function utf8($value)
+ {
+ if (is_string($value))
+ return utf8_encode($value);
+ else
+ return $value;
+ }
+
+ private static function _encodeObjects($d)
+ {
+ if ($d instanceof Stripe_ApiRequestor) {
+ return $d->id;
+ } else if ($d === true) {
+ return 'true';
+ } else if ($d === false) {
+ return 'false';
+ } else if (is_array($d)) {
+ $res = array();
+ foreach ($d as $k => $v)
+ $res[$k] = self::_encodeObjects($v);
+ return $res;
+ } else {
+ return $d;
+ }
+ }
+
+ public static function encode($d)
+ {
+ return http_build_query($d, null, '&');
+ }
+
+ public function request($meth, $url, $params=null)
+ {
+ if (!$params)
+ $params = array();
+ list($rbody, $rcode, $myApiKey) = $this->_requestRaw($meth, $url, $params);
+ $resp = $this->_interpretResponse($rbody, $rcode);
+ return array($resp, $myApiKey);
+ }
+
+ public function handleApiError($rbody, $rcode, $resp)
+ {
+ if (!is_array($resp) || !isset($resp['error']))
+ throw new Stripe_ApiError("Invalid response object from API: $rbody (HTTP response code was $rcode)");
+ $error = $resp['error'];
+ switch ($rcode) {
+ case 400:
+ case 404:
+ throw new Stripe_InvalidRequestError(isset($error['message']) ? $error['message'] : null,
+ isset($error['param']) ? $error['param'] : null);
+ case 401:
+ throw new Stripe_AuthenticationError(isset($error['message']) ? $error['message'] : null);
+ case 402:
+ throw new Stripe_CardError(isset($error['message']) ? $error['message'] : null,
+ isset($error['param']) ? $error['param'] : null,
+ isset($error['code']) ? $error['code'] : null);
+ default:
+ throw new Stripe_ApiError(isset($error['message']) ? $error['message'] : null);
+ }
+ }
+
+ private function _requestRaw($meth, $url, $params)
+ {
+ $myApiKey = $this->_apiKey;
+ if (!$myApiKey)
+ $myApiKey = Stripe::$apiKey;
+ if (!$myApiKey)
+ throw new Stripe_AuthenticationError('No API key provided. (HINT: set your API key using "Stripe::setApiKey()". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions.');
+
+ $absUrl = $this->apiUrl($url);
+ $params = Stripe_Util::arrayClone($params);
+ $params = self::_encodeObjects($params);
+ $langVersion = phpversion();
+ $uname = php_uname();
+ $ua = array('bindings_version' => Stripe::VERSION,
+ 'lang' => 'php',
+ 'lang_version' => $langVersion,
+ 'publisher' => 'stripe',
+ 'uname' => $uname);
+ $headers = array('X-Stripe-Client-User-Agent: ' . json_encode($ua),
+ 'User-Agent: Stripe/v1 PhpBindings/' . Stripe::VERSION);
+ list($rbody, $rcode) = $this->_curlRequest($meth, $absUrl, $headers, $params, $myApiKey);
+ return array($rbody, $rcode, $myApiKey);
+ }
+
+ private function _interpretResponse($rbody, $rcode)
+ {
+ try {
+ $resp = json_decode($rbody, true);
+ } catch (Exception $e) {
+ throw new Stripe_ApiError("Invalid response body from API: $rbody (HTTP response code was $rcode)");
+ }
+
+ if ($rcode < 200 || $rcode >= 300) {
+ $this->handleApiError($rbody, $rcode, $resp);
+ }
+ return $resp;
+ }
+
+ private function _curlRequest($meth, $absUrl, $headers, $params, $myApiKey)
+ {
+ $curl = curl_init();
+ $meth = strtolower($meth);
+ $opts = array();
+ if ($meth == 'get') {
+ $opts[CURLOPT_HTTPGET] = 1;
+ if (count($params) > 0) {
+ $encoded = self::encode($params);
+ $absUrl = "$absUrl?$encoded";
+ }
+ } else if ($meth == 'post') {
+ $opts[CURLOPT_POST] = 1;
+ $opts[CURLOPT_POSTFIELDS] = self::encode($params);
+ } else if ($meth == 'delete') {
+ $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
+ if (count($params) > 0) {
+ $encoded = self::encode($params);
+ $absUrl = "$absUrl?$encoded";
+ }
+ } else {
+ throw new Stripe_ApiError("Unrecognized method $meth");
+ }
+
+ $absUrl = self::utf8($absUrl);
+ $opts[CURLOPT_URL] = $absUrl;
+ $opts[CURLOPT_RETURNTRANSFER] = true;
+ $opts[CURLOPT_CONNECTTIMEOUT] = 30;
+ $opts[CURLOPT_TIMEOUT] = 80;
+ $opts[CURLOPT_RETURNTRANSFER] = true;
+ $opts[CURLOPT_HTTPHEADER] = $headers;
+ $opts[CURLOPT_USERPWD] = $myApiKey . ':';
+ $opts[CURLOPT_CAINFO] = dirname(__FILE__) . '/../data/ca-certificates.crt';
+ if (!Stripe::$verifySslCerts)
+ $opts[CURLOPT_SSL_VERIFYPEER] = false;
+
+ curl_setopt_array($curl, $opts);
+ $rbody = curl_exec($curl);
+
+ if ($rbody === false) {
+ $errno = curl_errno($curl);
+ $message = curl_error($curl);
+ curl_close($curl);
+ $this->handleCurlError($errno, $message);
+ }
+
+ $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ return array($rbody, $rcode);
+ }
+
+ public function handleCurlError($errno, $message)
+ {
+ $apiBase = Stripe::$apiBase;
+ switch ($errno) {
+ case CURLE_COULDNT_CONNECT:
+ case CURLE_COULDNT_RESOLVE_HOST:
+ case CURLE_OPERATION_TIMEOUTED:
+ $msg = "Could not connect to Stripe ($apiBase). Please check your internet connection and try again. If this problem persists, you should check Stripe's service status at https://twitter.com/stripe, or let us know at support@stripe.com.";
+ break;
+ case CURLE_SSL_CACERT:
+ case CURLE_SSL_PEER_CERTIFICATE:
+ $msg = "Could not verify Stripe's SSL certificate. Please make sure that your network is not intercepting certificates. (Try going to $apiBase in your browser.) If this problem persists, let us know at support@stripe.com.";
+ break;
+ default:
+ $msg = "Unexpected error communicating with Stripe. If this problem persists, let us know at support@stripe.com.";
+ }
+
+ $msg .= "\n\n(Network error: $message)";
+ throw new Stripe_ApiConnectionError($msg);
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/ApiResource.php b/app/modules/billing/libraries/stripe/Stripe/ApiResource.php
new file mode 100644
index 00000000..1b513ac8
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/ApiResource.php
@@ -0,0 +1,102 @@
+refresh();
+ return $instance;
+ }
+
+ public function refresh()
+ {
+ $requestor = new Stripe_ApiRequestor($this->_apiKey);
+ $url = $this->instanceUrl();
+ list($response, $apiKey) = $requestor->request('get', $url);
+ $this->refreshFrom($response, $apiKey);
+ return $this;
+ }
+
+ public static function classUrl($class)
+ {
+ // Useful for namespaces: Foo\Stripe_Charge
+ if ($postfix = strrchr($class, '\\'))
+ $class = substr($postfix, 1);
+ if (substr($class, 0, strlen('Stripe')) == 'Stripe')
+ $class = substr($class, strlen('Stripe'));
+ $class = str_replace('_', '', $class);
+ $name = urlencode($class);
+ $name = strtolower($name);
+ return "/${name}s";
+ }
+
+ public function instanceUrl()
+ {
+ $id = $this['id'];
+ $class = get_class($this);
+ if (!$id) {
+ throw new Stripe_InvalidRequestError("Could not determine which URL to request: $class instance has invalid ID: $id");
+ }
+ $id = Stripe_ApiRequestor::utf8($id);
+ $base = self::classUrl($class);
+ $extn = urlencode($id);
+ return "$base/$extn";
+ }
+
+ private static function _validateCall($method, $params=null, $apiKey=null)
+ {
+ if ($params && !is_array($params))
+ throw new Stripe_Error("You must pass an array as the first argument to Stripe API method calls. (HINT: an example call to create a charge would be: \"StripeCharge::create(array('amount' => 100, 'currency' => 'usd', 'card' => array('number' => 4242424242424242, 'exp_month' => 5, 'exp_year' => 2015)))\")");
+ if ($apiKey && !is_string($apiKey))
+ throw new Stripe_Error('The second argument to Stripe API method calls is an optional per-request apiKey, which must be a string. (HINT: you can set a global apiKey by "Stripe::setApiKey()")');
+ }
+
+ protected static function _scopedAll($class, $params=null, $apiKey=null)
+ {
+ self::_validateCall('all', $params, $apiKey);
+ $requestor = new Stripe_ApiRequestor($apiKey);
+ $url = self::classUrl($class);
+ list($response, $apiKey) = $requestor->request('get', $url, $params);
+ return Stripe_Util::convertToStripeObject($response, $apiKey);
+ }
+
+ protected static function _scopedCreate($class, $params=null, $apiKey=null)
+ {
+ self::_validateCall('create', $params, $apiKey);
+ $requestor = new Stripe_ApiRequestor($apiKey);
+ $url = self::classUrl($class);
+ list($response, $apiKey) = $requestor->request('post', $url, $params);
+ return Stripe_Util::convertToStripeObject($response, $apiKey);
+ }
+
+ protected function _scopedSave($class)
+ {
+ self::_validateCall('save');
+ if ($this->_unsavedValues) {
+ $requestor = new Stripe_ApiRequestor($this->_apiKey);
+ $params = array();
+ foreach ($this->_unsavedValues->toArray() as $k)
+ $params[$k] = $this->$k;
+ $url = $this->instanceUrl();
+ list($response, $apiKey) = $requestor->request('post', $url, $params);
+ $this->refreshFrom($response, $apiKey);
+ }
+ return $this;
+ }
+
+ protected function _scopedDelete($class, $params=null)
+ {
+ self::_validateCall('delete');
+ $requestor = new Stripe_ApiRequestor($this->_apiKey);
+ $url = $this->instanceUrl();
+ list($response, $apiKey) = $requestor->request('delete', $url, $params);
+ $this->refreshFrom($response, $apiKey);
+ return $this;
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/AuthenticationError.php b/app/modules/billing/libraries/stripe/Stripe/AuthenticationError.php
new file mode 100644
index 00000000..0c820305
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/AuthenticationError.php
@@ -0,0 +1,5 @@
+param = $param;
+ $this->code = $code;
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/Charge.php b/app/modules/billing/libraries/stripe/Stripe/Charge.php
new file mode 100644
index 00000000..20da57f0
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/Charge.php
@@ -0,0 +1,46 @@
+_apiKey);
+ $url = $this->instanceUrl() . '/refund';
+ list($response, $apiKey) = $requestor->request('post', $url, $params);
+ $this->refreshFrom($response, $apiKey);
+ return $this;
+ }
+
+ public function capture($params=null)
+ {
+ $requestor = new Stripe_ApiRequestor($this->_apiKey);
+ $url = $this->instanceUrl() . '/capture';
+ list($response, $apiKey) = $requestor->request('post', $url, $params);
+ $this->refreshFrom($response, $apiKey);
+ return $this;
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/Coupon.php b/app/modules/billing/libraries/stripe/Stripe/Coupon.php
new file mode 100644
index 00000000..eca24b49
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/Coupon.php
@@ -0,0 +1,34 @@
+id;
+ $ii = Stripe_InvoiceItem::create($params, $this->_apiKey);
+ return $ii;
+ }
+
+ public function invoices($params=null)
+ {
+ if (!$params)
+ $params = array();
+ $params['customer'] = $this->id;
+ $invoices = Stripe_Invoice::all($params, $this->_apiKey);
+ return $invoices;
+ }
+
+ public function invoiceItems($params=null)
+ {
+ if (!$params)
+ $params = array();
+ $params['customer'] = $this->id;
+ $iis = Stripe_InvoiceItem::all($params, $this->_apiKey);
+ return $iis;
+ }
+
+ public function charges($params=null)
+ {
+ if (!$params)
+ $params = array();
+ $params['customer'] = $this->id;
+ $charges = Stripe_Charge::all($params, $this->_apiKey);
+ return $charges;
+ }
+
+ public function updateSubscription($params=null)
+ {
+ $requestor = new Stripe_ApiRequestor($this->_apiKey);
+ $url = $this->instanceUrl() . '/subscription';
+ list($response, $apiKey) = $requestor->request('post', $url, $params);
+ $this->refreshFrom(array('subscription' => $response), $apiKey, true);
+ return $this->subscription;
+ }
+
+ public function cancelSubscription($params=null)
+ {
+ $requestor = new Stripe_ApiRequestor($this->_apiKey);
+ $url = $this->instanceUrl() . '/subscription';
+ list($response, $apiKey) = $requestor->request('delete', $url, $params);
+ $this->refreshFrom(array('subscription' => $response), $apiKey, true);
+ return $this->subscription;
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/Error.php b/app/modules/billing/libraries/stripe/Stripe/Error.php
new file mode 100644
index 00000000..64c2569f
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/Error.php
@@ -0,0 +1,5 @@
+param = $param;
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/Invoice.php b/app/modules/billing/libraries/stripe/Stripe/Invoice.php
new file mode 100644
index 00000000..80152b75
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/Invoice.php
@@ -0,0 +1,30 @@
+request('get', $url, $params);
+ return Stripe_Util::convertToStripeObject($response, $apiKey);
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/InvoiceItem.php b/app/modules/billing/libraries/stripe/Stripe/InvoiceItem.php
new file mode 100644
index 00000000..549954ba
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/InvoiceItem.php
@@ -0,0 +1,40 @@
+_apiKey = $apiKey;
+ $this->_values = array();
+ $this->_unsavedValues = new Stripe_Util_Set();
+ $this->_transientValues = new Stripe_Util_Set();
+ if ($id)
+ $this->id = $id;
+ }
+
+ // Standard accessor magic methods
+ public function __set($k, $v)
+ {
+ // TODO: may want to clear from $_transientValues. (Won't be user-visible.)
+ $this->_values[$k] = $v;
+ if (!self::$_ignoredAttributes->includes($k))
+ $this->_unsavedValues->add($k);
+ }
+ public function __isset($k)
+ {
+ return isset($this->_values[$k]);
+ }
+ public function __unset($k)
+ {
+ unset($this->_values[$k]);
+ $this->_transientValues->add($k);
+ $this->_unsavedValues->discard($k);
+ }
+ public function __get($k)
+ {
+ if (isset($this->_values[$k])) {
+ return $this->_values[$k];
+ } else if ($this->_transientValues->includes($k)) {
+ $class = get_class($this);
+ $attrs = join(', ', array_keys($this->_values));
+ error_log("Stripe Notice: Undefined property of $class instance: $k. HINT: The $k attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Stripe's API, probably as a result of a save(). The attributes currently available on this object are: $attrs");
+ return null;
+ } else {
+ $class = get_class($this);
+ error_log("Stripe Notice: Undefined property of $class instance: $k");
+ return null;
+ }
+ }
+
+ // ArrayAccess methods
+ public function offsetSet($k, $v)
+ {
+ $this->$k = $v;
+ }
+ public function offsetExists($k)
+ {
+ return isset($this->$k);
+ }
+ public function offsetUnset($k)
+ {
+ unset($this->$k);
+ }
+ public function offsetGet($k)
+ {
+ return isset($this->_values[$k]) ? $this->_values[$k] : null;
+ }
+
+ // This unfortunately needs to be public to be used in Util.php
+ public static function scopedConstructFrom($class, $values, $apiKey=null)
+ {
+ $obj = new $class(isset($values['id']) ? $values['id'] : null,
+ $apiKey);
+ $obj->refreshFrom($values, $apiKey);
+ return $obj;
+ }
+
+ public static function constructFrom($values, $apiKey=null)
+ {
+ $class = get_class();
+ return self::scopedConstructFrom($class, $values, $apiKey);
+ }
+
+ public function refreshFrom($values, $apiKey, $partial=false)
+ {
+ $this->_apiKey = $apiKey;
+ // Wipe old state before setting new. This is useful for e.g. updating a
+ // customer, where there is no persistent card parameter. Mark those values
+ // which don't persist as transient
+ if ($partial)
+ $removed = new Stripe_Util_Set();
+ else
+ $removed = array_diff(array_keys($this->_values), array_keys($values));
+
+ foreach ($removed as $k) {
+ if (self::$_permanentAttributes->includes($k))
+ continue;
+ unset($this->$k);
+ }
+
+ foreach ($values as $k => $v) {
+ if (self::$_permanentAttributes->includes($k))
+ continue;
+ $this->_values[$k] = Stripe_Util::convertToStripeObject($v, $apiKey);
+ $this->_transientValues->discard($k);
+ $this->_unsavedValues->discard($k);
+ }
+ }
+
+ protected function _ident()
+ {
+ return array($this['object'], $this['id']);
+ }
+
+ protected function _stringify($nested=false)
+ {
+ $ident = array_filter($this->_ident());
+ if ($ident)
+ $ident = '[' . join(', ', $ident) . ']';
+ else
+ $ident = '';
+ $class = get_class($this);
+
+ if ($nested)
+ return "<$class$ident ...>";
+
+ $valuesStr = array();
+ $values = Stripe_Util::arrayClone($this->_values);
+ ksort($values);
+ foreach ($values as $k => $v) {
+ if (self::$_ignoredAttributes->includes($k))
+ continue;
+ $v = $this->$k;
+ if ($v instanceof Stripe_Object)
+ $v = $v->_stringify(true);
+ else if (is_bool($v))
+ $v = $v ? 'true' : 'false';
+ else
+ $v = "$v";
+ if ($this->_unsavedValues->includes($k))
+ array_push($valuesStr, "$k=$v (unsaved)");
+ else
+ array_push($valuesStr, "$k=$v");
+ }
+ if (count($valuesStr) == 0)
+ array_push($valuesStr, '(no attributes)');
+ $displayValues = join(', ', $valuesStr);
+ return "<$class$ident $displayValues>";
+ }
+
+ public function __toString()
+ {
+ return $this->_stringify();
+ }
+
+ // Convert this object to an array. (Note that this method does not
+ // recurse, so any contained Stripe_Objects will be present in the
+ // returned array.
+ public function __toArray()
+ {
+ return $this->_values;
+ }
+}
+
+
+Stripe_Object::init();
diff --git a/app/modules/billing/libraries/stripe/Stripe/Plan.php b/app/modules/billing/libraries/stripe/Stripe/Plan.php
new file mode 100644
index 00000000..f75968af
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/Plan.php
@@ -0,0 +1,34 @@
+ 'Stripe_Charge',
+ 'customer' => 'Stripe_Customer',
+ 'invoice' => 'Stripe_Invoice',
+ 'invoiceitem' => 'Stripe_InvoiceItem');
+ if (self::isList($resp)) {
+ $mapped = array();
+ foreach ($resp as $i)
+ array_push($mapped, self::convertToStripeObject($i, $apiKey));
+ return $mapped;
+ } else if (is_array($resp)) {
+ $resp = self::arrayClone($resp);
+ if (isset($resp['object']) && isset($types[$resp['object']]))
+ $class = $types[$resp['object']];
+ else
+ $class = 'Stripe_Object';
+ return Stripe_Object::scopedConstructFrom($class, $resp, $apiKey);
+ } else {
+ return $resp;
+ }
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/Stripe/Util/Set.php b/app/modules/billing/libraries/stripe/Stripe/Util/Set.php
new file mode 100644
index 00000000..e2654830
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/Stripe/Util/Set.php
@@ -0,0 +1,34 @@
+_elts = array();
+ foreach ($members as $item)
+ $this->_elts[$item] = true;
+ }
+
+ public function includes($elt)
+ {
+ return isset($this->_elts[$elt]);
+ }
+
+ public function add($elt)
+ {
+ $this->_elts[$elt] = true;
+ }
+
+ public function discard($elt)
+ {
+ unset($this->_elts[$elt]);
+ }
+
+ // TODO: make Set support foreach
+ public function toArray()
+ {
+ return array_keys($this->_elts);
+ }
+}
diff --git a/app/modules/billing/libraries/stripe/data/ca-certificates.crt b/app/modules/billing/libraries/stripe/data/ca-certificates.crt
new file mode 100644
index 00000000..503ffd8d
--- /dev/null
+++ b/app/modules/billing/libraries/stripe/data/ca-certificates.crt
@@ -0,0 +1,3684 @@
+# Generated using the default CA bundle on Ubuntu Linux 10.04 on Monday, May 9, 2011
+
+-----BEGIN CERTIFICATE-----
+MIIEuDCCA6CgAwIBAgIBBDANBgkqhkiG9w0BAQUFADCBtDELMAkGA1UEBhMCQlIx
+EzARBgNVBAoTCklDUC1CcmFzaWwxPTA7BgNVBAsTNEluc3RpdHV0byBOYWNpb25h
+bCBkZSBUZWNub2xvZ2lhIGRhIEluZm9ybWFjYW8gLSBJVEkxETAPBgNVBAcTCEJy
+YXNpbGlhMQswCQYDVQQIEwJERjExMC8GA1UEAxMoQXV0b3JpZGFkZSBDZXJ0aWZp
+Y2Fkb3JhIFJhaXogQnJhc2lsZWlyYTAeFw0wMTExMzAxMjU4MDBaFw0xMTExMzAy
+MzU5MDBaMIG0MQswCQYDVQQGEwJCUjETMBEGA1UEChMKSUNQLUJyYXNpbDE9MDsG
+A1UECxM0SW5zdGl0dXRvIE5hY2lvbmFsIGRlIFRlY25vbG9naWEgZGEgSW5mb3Jt
+YWNhbyAtIElUSTERMA8GA1UEBxMIQnJhc2lsaWExCzAJBgNVBAgTAkRGMTEwLwYD
+VQQDEyhBdXRvcmlkYWRlIENlcnRpZmljYWRvcmEgUmFpeiBCcmFzaWxlaXJhMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPMudwX/hvm+Uh2b/lQAcHVA
+isamaLkWdkwP9/S/tOKIgRrL6Oy+ZIGlOUdd6uYtk9Ma/3pUpgcfNAj0vYm5gsyj
+Qo9emsc+x6m4VWwk9iqMZSCK5EQkAq/Ut4n7KuLE1+gdftwdIgxfUsPt4CyNrY50
+QV57KM2UT8x5rrmzEjr7TICGpSUAl2gVqe6xaii+bmYR1QrmWaBSAG59LrkrjrYt
+bRhFboUDe1DK+6T8s5L6k8c8okpbHpa9veMztDVC9sPJ60MWXh6anVKo1UcLcbUR
+yEeNvZneVRKAAU6ouwdjDvwlsaKydFKwed0ToQ47bmUKgcm+wV3eTRk36UOnTwID
+AQABo4HSMIHPME4GA1UdIARHMEUwQwYFYEwBAQAwOjA4BggrBgEFBQcCARYsaHR0
+cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJyL0RQQ2FjcmFpei5wZGYwPQYDVR0f
+BDYwNDAyoDCgLoYsaHR0cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJyL0xDUmFj
+cmFpei5jcmwwHQYDVR0OBBYEFIr68VeEERM1kEL6V0lUaQ2kxPA3MA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAZA5c1
+U/hgIh6OcgLAfiJgFWpvmDZWqlV30/bHFpj8iBobJSm5uDpt7TirYh1Uxe3fQaGl
+YjJe+9zd+izPRbBqXPVQA34EXcwk4qpWuf1hHriWfdrx8AcqSqr6CuQFwSr75Fos
+SzlwDADa70mT7wZjAmQhnZx2xJ6wfWlT9VQfS//JYeIc7Fue2JNLd00UOSMMaiK/
+t79enKNHEA2fupH3vEigf5Eh4bVAN5VohrTm6MY53x7XQZZr1ME7a55lFEnSeT0u
+mlOAjR2mAbvSM5X5oSZNrmetdzyTj2flCM8CC7MLab0kkdngRIlUBGHF1/S5nmPb
+K+9A46sd33oqK8n8
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
+IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
+IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
+Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO
+BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi
+MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
+ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ
+8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6
+zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y
+fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7
+w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc
+G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k
+epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q
+laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ
+QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU
+fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826
+YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
+ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY
+gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe
+MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0
+IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy
+dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw
+czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0
+dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl
+aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC
+AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg
+b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB
+ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc
+nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg
+18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c
+gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl
+Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY
+sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T
+SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF
+CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
+GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk
+zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW
+omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGCDCCA/CgAwIBAgIBATANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
+IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
+IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
+Y2FjZXJ0Lm9yZzAeFw0wNTEwMTQwNzM2NTVaFw0zMzAzMjgwNzM2NTVaMFQxFDAS
+BgNVBAoTC0NBY2VydCBJbmMuMR4wHAYDVQQLExVodHRwOi8vd3d3LkNBY2VydC5v
+cmcxHDAaBgNVBAMTE0NBY2VydCBDbGFzcyAzIFJvb3QwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCrSTURSHzSJn5TlM9Dqd0o10Iqi/OHeBlYfA+e2ol9
+4fvrcpANdKGWZKufoCSZc9riVXbHF3v1BKxGuMO+f2SNEGwk82GcwPKQ+lHm9WkB
+Y8MPVuJKQs/iRIwlKKjFeQl9RrmK8+nzNCkIReQcn8uUBByBqBSzmGXEQ+xOgo0J
+0b2qW42S0OzekMV/CsLj6+YxWl50PpczWejDAz1gM7/30W9HxM3uYoNSbi4ImqTZ
+FRiRpoWSR7CuSOtttyHshRpocjWr//AQXcD0lKdq1TuSfkyQBX6TwSyLpI5idBVx
+bgtxA+qvFTia1NIFcm+M+SvrWnIl+TlG43IbPgTDZCciECqKT1inA62+tC4T7V2q
+SNfVfdQqe1z6RgRQ5MwOQluM7dvyz/yWk+DbETZUYjQ4jwxgmzuXVjit89Jbi6Bb
+6k6WuHzX1aCGcEDTkSm3ojyt9Yy7zxqSiuQ0e8DYbF/pCsLDpyCaWt8sXVJcukfV
+m+8kKHA4IC/VfynAskEDaJLM4JzMl0tF7zoQCqtwOpiVcK01seqFK6QcgCExqa5g
+eoAmSAC4AcCTY1UikTxW56/bOiXzjzFU6iaLgVn5odFTEcV7nQP2dBHgbbEsPyyG
+kZlxmqZ3izRg0RS0LKydr4wQ05/EavhvE/xzWfdmQnQeiuP43NJvmJzLR5iVQAX7
+6QIDAQABo4G/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGAQUFBzABhhdodHRwOi8vb2NzcC5DQWNlcnQub3JnLzAoBggrBgEFBQcwAoYc
+aHR0cDovL3d3dy5DQWNlcnQub3JnL2NhLmNydDBKBgNVHSAEQzBBMD8GCCsGAQQB
+gZBKMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuQ0FjZXJ0Lm9yZy9pbmRleC5w
+aHA/aWQ9MTAwDQYJKoZIhvcNAQEEBQADggIBAH8IiKHaGlBJ2on7oQhy84r3HsQ6
+tHlbIDCxRd7CXdNlafHCXVRUPIVfuXtCkcKZ/RtRm6tGpaEQU55tiKxzbiwzpvD0
+nuB1wT6IRanhZkP+VlrRekF490DaSjrxC1uluxYG5sLnk7mFTZdPsR44Q4Dvmw2M
+77inYACHV30eRBzLI++bPJmdr7UpHEV5FpZNJ23xHGzDwlVks7wU4vOkHx4y/CcV
+Bc/dLq4+gmF78CEQGPZE6lM5+dzQmiDgxrvgu1pPxJnIB721vaLbLmINQjRBvP+L
+ivVRIqqIMADisNS8vmW61QNXeZvo3MhN+FDtkaVSKKKs+zZYPumUK5FQhxvWXtaM
+zPcPEAxSTtAWYeXlCmy/F8dyRlecmPVsYGN6b165Ti/Iubm7aoW8mA3t+T6XhDSU
+rgCvoeXnkm5OvfPi2RSLXNLrAWygF6UtEOucekq9ve7O/e0iQKtwOIj1CodqwqsF
+YMlIBdpTwd5Ed2qz8zw87YC8pjhKKSRf/lk7myV6VmMAZLldpGJ9VzZPrYPvH5JT
+oI53V93lYRE9IwCQTDz6o2CTBKOvNfYOao9PSmCnhQVsRqGP9Md246FZV/dxssRu
+FFxtbUFm3xuTsdQAw+7Lzzw9IYCpX2Nl/N3gX6T0K/CFcUHUZyX7GrGXrtaZghNB
+0m6lG5kngOcLqagA
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIESzCCAzOgAwIBAgIJAJigUTEEXRQpMA0GCSqGSIb3DQEBBQUAMHYxCzAJBgNV
+BAYTAkRFMQ8wDQYDVQQIEwZIZXNzZW4xDjAMBgNVBAcTBUZ1bGRhMRAwDgYDVQQK
+EwdEZWJjb25mMRMwEQYDVQQDEwpEZWJjb25mIENBMR8wHQYJKoZIhvcNAQkBFhBq
+b2VyZ0BkZWJpYW4ub3JnMB4XDTA1MTEwNTE3NTUxNFoXDTE1MTEwMzE3NTUxNFow
+djELMAkGA1UEBhMCREUxDzANBgNVBAgTBkhlc3NlbjEOMAwGA1UEBxMFRnVsZGEx
+EDAOBgNVBAoTB0RlYmNvbmYxEzARBgNVBAMTCkRlYmNvbmYgQ0ExHzAdBgkqhkiG
+9w0BCQEWEGpvZXJnQGRlYmlhbi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQCvbOo0SrIwI5IMlsshH8WF3dHB9r9JlSKhMPaybawa1EyvZspMQ3wa
+F5qxNf3Sj+NElEmjseEqvCZiIIzqwerHu0Qw62cDYCdCd2+Wb5m0bPYB5CGHiyU1
+eNP0je42O0YeXG2BvUujN8AviocVo39X2YwNQ0ryy4OaqYgm2pRlbtT2ESbF+SfV
+Y2iqQj/f8ymF+lHo/pz8tbAqxWcqaSiHFAVQJrdqtFhtoodoNiE3q76zJoUkZTXB
+k60Yc3MJSnatZCpnsSBr/D7zpntl0THrUjjtdRWCjQVhqfhM1yZJV+ApbLdheFh0
+ZWlSxdnp25p0q0XYw/7G92ELyFDfBUUNAgMBAAGjgdswgdgwHQYDVR0OBBYEFMuV
+dFNb4mCWUFbcP5LOtxFLrEVTMIGoBgNVHSMEgaAwgZ2AFMuVdFNb4mCWUFbcP5LO
+txFLrEVToXqkeDB2MQswCQYDVQQGEwJERTEPMA0GA1UECBMGSGVzc2VuMQ4wDAYD
+VQQHEwVGdWxkYTEQMA4GA1UEChMHRGViY29uZjETMBEGA1UEAxMKRGViY29uZiBD
+QTEfMB0GCSqGSIb3DQEJARYQam9lcmdAZGViaWFuLm9yZ4IJAJigUTEEXRQpMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGZXxHg4mnkvilRIM1EQfGdY
+S5b/WcyF2MYSTeTvK4aIB6VHwpZoZCnDGj2m2D3CkHT0upAD9o0zM1tdsfncLzV+
+mDT/jNmBtYo4QXx5vEPwvEIcgrWjwk7SyaEUhZjtolTkHB7ACl0oD0r71St4iEPR
+qTUCEXk2E47bg1Fz58wNt/yo2+4iqiRjg1XCH4evkQuhpW+dTZnDyFNqwSYZapOE
+TBA+9zBb6xD1KM2DdY7r4GiyYItN0BKLfuWbh9LXGbl1C+f4P11g+m2MPiavIeCe
+1iazG5pcS3KoTLACsYlEX24TINtg4kcuS81XdllcnsV3Kdts0nIqPj6uhTTZD0k=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDvjCCA3ygAwIBAgIFJQaThoEwCwYHKoZIzjgEAwUAMIGFMQswCQYDVQQGEwJG
+UjEPMA0GA1UECBMGRnJhbmNlMQ4wDAYDVQQHEwVQYXJpczEQMA4GA1UEChMHUE0v
+U0dETjEOMAwGA1UECxMFRENTU0kxDjAMBgNVBAMTBUlHQy9BMSMwIQYJKoZIhvcN
+AQkBFhRpZ2NhQHNnZG4ucG0uZ291di5mcjAeFw0wMjEyMTMxNDM5MTVaFw0yMDEw
+MTcxNDM5MTRaMIGFMQswCQYDVQQGEwJGUjEPMA0GA1UECBMGRnJhbmNlMQ4wDAYD
+VQQHEwVQYXJpczEQMA4GA1UEChMHUE0vU0dETjEOMAwGA1UECxMFRENTU0kxDjAM
+BgNVBAMTBUlHQy9BMSMwIQYJKoZIhvcNAQkBFhRpZ2NhQHNnZG4ucG0uZ291di5m
+cjCCAbYwggErBgcqhkjOOAQBMIIBHgKBgQCFkMImdk9zDzJfTO4XPdAAmLbAdWws
+ZiEMZh19RyTo3CyhFqO77OIXrwY6vc1pcc3MgWJ0dgQpAgrDMtmFFxpUu4gmjVsx
+8GpxQC+4VOgLY8Cvmcd/UDzYg07EIRto8BwCpPJ/JfUxwzV2V3N713aAX+cEoKZ/
+s+kgxC6nZCA7oQIVALME/JYjkdW2uKIGngsEPbXAjdhDAoGADh/uqWJx94UBm31c
+9d8ZTBfRGRnmSSRVFDgPWgA69JD4BR5da8tKz+1HjfMhDXljbMH86ixpD5Ka1Z0V
+pRYUPbyAoB37tsmXMJY7kjyD19d5VdaZboUjVvhH6UJy5lpNNNGSvFl4fqkxyvw+
+pq1QV0N5RcvK120hlXdfHUX+YKYDgYQAAoGAQGr7IuKJcYIvJRMjxwl43KxXY2xC
+aoCiM/bv117MfI94aNf1UusGhp7CbYAY9CXuL60P0oPMAajbaTE5Z34AuITeHq3Y
+CNMHwxalip8BHqSSGmGiQsXeK7T+r1rPXsccZ1c5ikGDZ4xn5gUaCyy2rCmb+fOJ
+6VAfCbAbAjmNKwejdzB1MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgFGMBUG
+A1UdIAQOMAwwCgYIKoF6AXkBAQEwHQYDVR0OBBYEFPkeNRcUf8idzpKblYbLNxs0
+MQhSMB8GA1UdIwQYMBaAFPkeNRcUf8idzpKblYbLNxs0MQhSMAsGByqGSM44BAMF
+AAMvADAsAhRVh+CJA5eVyEYU5AO9Tm7GxX0rmQIUBCqsU5u1WxoZ5lEXicDX5/Ob
+sRQ=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT
+AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ
+TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG
+9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw
+MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM
+BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO
+MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2
+LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI
+s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2
+xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4
+u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b
+F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx
+Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd
+PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV
+HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx
+NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF
+AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ
+L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY
+YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg
+Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a
+NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R
+0982gaEbeC9xs/FZTEYYKKuF0mBWWg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDtTCCAp2gAwIBAgIRANAeQJAAAEZSAAAAAQAAAAQwDQYJKoZIhvcNAQEFBQAw
+gYkxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJEQzETMBEGA1UEBxMKV2FzaGluZ3Rv
+bjEXMBUGA1UEChMOQUJBLkVDT00sIElOQy4xGTAXBgNVBAMTEEFCQS5FQ09NIFJv
+b3QgQ0ExJDAiBgkqhkiG9w0BCQEWFWFkbWluQGRpZ3NpZ3RydXN0LmNvbTAeFw05
+OTA3MTIxNzMzNTNaFw0wOTA3MDkxNzMzNTNaMIGJMQswCQYDVQQGEwJVUzELMAkG
+A1UECBMCREMxEzARBgNVBAcTCldhc2hpbmd0b24xFzAVBgNVBAoTDkFCQS5FQ09N
+LCBJTkMuMRkwFwYDVQQDExBBQkEuRUNPTSBSb290IENBMSQwIgYJKoZIhvcNAQkB
+FhVhZG1pbkBkaWdzaWd0cnVzdC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQCx0xHgeVVDBwhMywVCAOINg0Y95JO6tgbTDVm9PsHOQ2cBiiGo77zM
+0KLMsFWWU4RmBQDaREmA2FQKpSWGlO1jVv9wbKOhGdJ4vmgqRF4vz8wYXke8OrFG
+PR7wuSw0X4x8TAgpnUBV6zx9g9618PeKgw6hTLQ6pbNfWiKX7BmbwQVo/ea3qZGU
+LOR4SCQaJRk665WcOQqKz0Ky8BzVX/tr7WhWezkscjiw7pOp03t3POtxA6k4ShZs
+iSrK2jMTecJVjO2cu/LLWxD4LmE1xilMKtAqY9FlWbT4zfn0AIS2V0KFnTKo+SpU
++/94Qby9cSj0u5C8/5Y0BONFnqFGKECBAgMBAAGjFjAUMBIGA1UdEwEB/wQIMAYB
+Af8CAQgwDQYJKoZIhvcNAQEFBQADggEBAARvJYbk5pYntNlCwNDJALF/VD6Hsm0k
+qS8Kfv2kRLD4VAe9G52dyntQJHsRW0mjpr8SdNWJt7cvmGQlFLdh6X9ggGvTZOir
+vRrWUfrAtF13Gn9kCF55xgVM8XrdTX3O5kh7VNJhkoHWG9YA8A6eKHegTYjHInYZ
+w8eeG6Z3ePhfm1bR8PIXrI6dWeYf/le22V7hXZ9F7GFoGUHhsiAm/lowdiT/QHI8
+eZ98IkirRs3bs4Ysj78FQdPB4xTjQRcm0HyncUwZ6EoPclgxfexgeqMiKL0ZJGA/
+O4dzwGvky663qyVDslUte6sGDnVdNOVdc22esnVApVnJTzFxiNmIf1Q=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
+IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
+MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
+FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
+bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
+H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
+uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
+mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
+a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
+E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
+WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
+VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
+Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
+cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
+IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
+AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
+YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
+6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
+Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
+c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
+mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3
+b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw
+MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
+QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD
+VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul
+CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n
+tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl
+dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch
+PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC
++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O
+BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E
+BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl
+MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk
+ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB
+IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X
+7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz
+43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY
+eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl
+pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA
+WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3
+b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx
+MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB
+ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV
+BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV
+6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX
+GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP
+dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH
+1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF
+62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW
+BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw
+AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL
+MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU
+cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv
+b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6
+IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/
+iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao
+GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh
+4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm
+XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3
+b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1
+MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK
+EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh
+BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq
+xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G
+87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i
+2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U
+WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1
+0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G
+A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr
+pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL
+ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm
+aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv
+hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm
+hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X
+dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3
+P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y
+iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no
+xqE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP
+bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2
+MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft
+ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk
+hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym
+1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW
+OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb
+2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko
+O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU
+AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
+BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF
+Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb
+LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir
+oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C
+MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds
+sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP
+bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2
+MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft
+ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC
+206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci
+KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2
+JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9
+BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e
+Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B
+PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67
+Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq
+Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ
+o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3
++L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj
+YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj
+FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE
+AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn
+xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2
+LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc
+obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8
+CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe
+IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA
+DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F
+AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX
+Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb
+AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl
+Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw
+RY8mkaKO/qk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMCVVMx
+HTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNBbWVyaWNh
+IE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIgUm9vdCBDZXJ0
+aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyOTA2MDAwMFoXDTM3MTEyMDE1
+MDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRBT0wgVGltZSBXYXJuZXIg
+SW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUgSW5jLjE3MDUGA1UEAxMuQU9M
+IFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnej8Mlo2k06AX3dLm/WpcZuS+U
+0pPlLYnKhHw/EEMbjIt8hFj4JHxIzyr9wBXZGH6EGhfT257XyuTZ16pYUYfw8ItI
+TuLCxFlpMGK2MKKMCxGZYTVtfu/FsRkGIBKOQuHfD5YQUqjPnF+VFNivO3ULMSAf
+RC+iYkGzuxgh28pxPIzstrkNn+9R7017EvILDOGsQI93f7DKeHEMXRZxcKLXwjqF
+zQ6axOAAsNUl6twr5JQtOJyJQVdkKGUZHLZEtMgxa44Be3ZZJX8VHIQIfHNlIAqh
+BC4aMqiaILGcLCFZ5/vP7nAtCMpjPiybkxlqpMKX/7eGV4iFbJ4VFitNLLMCAwEA
+AaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUoTYwFsuGkABFgFOxj8jY
+PXy+XxIwHwYDVR0jBBgwFoAUoTYwFsuGkABFgFOxj8jYPXy+XxIwDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQCKIBilvrMvtKaEAEAwKfq0FHNMeUWn
+9nDg6H5kHgqVfGphwu9OH77/yZkfB2FK4V1Mza3u0FIy2VkyvNp5ctZ7CegCgTXT
+Ct8RHcl5oIBN/lrXVtbtDyqvpxh1MwzqwWEFT2qaifKNuZ8u77BfWgDrvq2g+EQF
+Z7zLBO+eZMXpyD8Fv8YvBxzDNnGGyjhmSs3WuEvGbKeXO/oTLW4jYYehY0KswsuX
+n2Fozy1MBJ3XJU8KDk2QixhWqJNIV9xvrr2eZ1d3iVCzvhGbRWeDhhmH05i9CBoW
+H1iCC+GWaQVLjuyDUTEH1dSf/1l7qG6Fz9NLqUmwX7A5KGgOc90lmt4S
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF5jCCA86gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMCVVMx
+HTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNBbWVyaWNh
+IE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIgUm9vdCBDZXJ0
+aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyOTA2MDAwMFoXDTM3MDkyODIz
+NDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRBT0wgVGltZSBXYXJuZXIg
+SW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUgSW5jLjE3MDUGA1UEAxMuQU9M
+IFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQ3WggWmRToVbEbJGv8x4vmh6mJ
+7ouZzU9AhqS2TcnZsdw8TQ2FTBVsRotSeJ/4I/1n9SQ6aF3Q92RhQVSji6UI0ilb
+m2BPJoPRYxJWSXakFsKlnUWsi4SVqBax7J/qJBrvuVdcmiQhLE0OcR+mrF1FdAOY
+xFSMFkpBd4aVdQxHAWZg/BXxD+r1FHjHDtdugRxev17nOirYlxcwfACtCJ0zr7iZ
+YYCLqJV+FNwSbKTQ2O9ASQI2+W6p1h2WVgSysy0WVoaP2SBXgM1nEG2wTPDaRrbq
+JS5Gr42whTg0ixQmgiusrpkLjhTXUr2eacOGAgvqdnUxCc4zGSGFQ+aJLZ8lN2fx
+I2rSAG2X+Z/nKcrdH9cG6rjJuQkhn8g/BsXS6RJGAE57COtCPStIbp1n3UsC5ETz
+kxmlJ85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86QNZ8MUhy/XCFh
+EVsVS6kkUfykXPcXnbDS+gfpj1bkGoxoigTTfFrjnqKhynFbotSg5ymFXQNoKk/S
+Btc9+cMDLz9l+WceR0DTYw/j1Y75hauXTLPXJuuWCpTehTacyH+BCQJJKg71ZDIM
+gtG6aoIbs0t0EfOMd9afv9w3pKdVBC/UMejTRrkDfNoSTllkt1ExMVCgyhwn2RAu
+rda9EGYrw7AiShJbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
+FE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1UdIwQYMBaAFE9pbQN+nZ8HGEO8txBO
+1b+pxCAoMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAO/Ouyugu
+h4X7ZVnnrREUpVe8WJ8kEle7+z802u6teio0cnAxa8cZmIDJgt43d15Ui47y6mdP
+yXSEkVYJ1eV6moG2gcKtNuTxVBFT8zRFASbI5Rq8NEQh3q0l/HYWdyGQgJhXnU7q
+7C+qPBR7V8F+GBRn7iTGvboVsNIYvbdVgaxTwOjdaRITQrcCtQVBynlQboIOcXKT
+RuidDV29rs4prWPVVRaAMCf/drr3uNZK49m1+VLQTkCpx+XCMseqdiThawVQ68W/
+ClTluUI8JPu3B5wwn3la5uBAUhX0/Kr0VvlEl4ftDmVyXr4m+02kLQgH3thcoNyB
+M5kYJRF3p+v9WAksmWsbivNSPxpNSGDxoPYzAlOL7SUJuA0t7Zdz7NeWH45gDtoQ
+my8YJPamTQr5O8t1wswvziRpyQoijlmn94IM19drNZxDAGrElWe6nEXLuA4399xO
+AU++CrYD062KRffaJ00psUjf5BHklka9bAI+1lHIlRcBFanyqqryvy9lG2/QuRqT
+9Y41xICHPpQvZuTpqP9BnHAqTyo5GJUefvthATxRCC4oGKQWDzH9OmwjkyB24f0H
+hdFbP9IcczLd+rn4jM8Ch3qaluTtT4mNU0OrDhPAARW0eTjb/G49nlG2uBOLZ8/5
+fNkiHfZdxRwBL5joeiQYvITX+txyW/fBOmg=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
+RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
+VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
+DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
+ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
+VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
+mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
+IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
+mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
+XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
+dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
+jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
+BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
+DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
+9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
+jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
+Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
+ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
+R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFajCCBFKgAwIBAgIEPLU9RjANBgkqhkiG9w0BAQUFADBmMRIwEAYDVQQKEwli
+ZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENBczEzMDEGA1UEAxMq
+YmVUUlVTVGVkIFJvb3QgQ0EtQmFsdGltb3JlIEltcGxlbWVudGF0aW9uMB4XDTAy
+MDQxMTA3Mzg1MVoXDTIyMDQxMTA3Mzg1MVowZjESMBAGA1UEChMJYmVUUlVTVGVk
+MRswGQYDVQQLExJiZVRSVVNUZWQgUm9vdCBDQXMxMzAxBgNVBAMTKmJlVFJVU1Rl
+ZCBSb290IENBLUJhbHRpbW9yZSBJbXBsZW1lbnRhdGlvbjCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBALx+xDmcjOPWHIb/ymKt4H8wRXqOGrO4x/nRNv8i
+805qX4QQ+2aBw5R5MdKR4XeOGCrDFN5R9U+jK7wYFuK13XneIviCfsuBH/0nLI/6
+l2Qijvj/YaOcGx6Sj8CoCd8JEey3fTGaGuqDIQY8n7pc/5TqarjDa1U0Tz0yH92B
+FODEPM2dMPgwqZfT7syj0B9fHBOB1BirlNFjw55/NZKeX0Tq7PQiXLfoPX2k+Ymp
+kbIq2eszh+6l/ePazIjmiSZuxyuC0F6dWdsU7JGDBcNeDsYq0ATdcT0gTlgn/FP7
+eHgZFLL8kFKJOGJgB7Sg7KxrUNb9uShr71ItOrL/8QFArDcCAwEAAaOCAh4wggIa
+MA8GA1UdEwEB/wQFMAMBAf8wggG1BgNVHSAEggGsMIIBqDCCAaQGDysGAQQBsT4A
+AAEJKIORMTCCAY8wggFIBggrBgEFBQcCAjCCAToaggE2UmVsaWFuY2Ugb24gb3Ig
+dXNlIG9mIHRoaXMgQ2VydGlmaWNhdGUgY3JlYXRlcyBhbiBhY2tub3dsZWRnbWVu
+dCBhbmQgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJk
+IHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgdGhlIENlcnRpZmljYXRpb24g
+UHJhY3RpY2UgU3RhdGVtZW50IGFuZCB0aGUgUmVseWluZyBQYXJ0eSBBZ3JlZW1l
+bnQsIHdoaWNoIGNhbiBiZSBmb3VuZCBhdCB0aGUgYmVUUlVTVGVkIHdlYiBzaXRl
+LCBodHRwOi8vd3d3LmJldHJ1c3RlZC5jb20vcHJvZHVjdHNfc2VydmljZXMvaW5k
+ZXguaHRtbDBBBggrBgEFBQcCARY1aHR0cDovL3d3dy5iZXRydXN0ZWQuY29tL3By
+b2R1Y3RzX3NlcnZpY2VzL2luZGV4Lmh0bWwwHQYDVR0OBBYEFEU9w6nR3D8kVpgc
+cxiIav+DR+22MB8GA1UdIwQYMBaAFEU9w6nR3D8kVpgccxiIav+DR+22MA4GA1Ud
+DwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEASZK8o+6svfoNyYt5hhwjdrCA
+WXf82n+0S9/DZEtqTg6t8n1ZdwWtColzsPq8y9yNAIiPpqCy6qxSJ7+hSHyXEHu6
+7RMdmgduyzFiEuhjA6p9beP4G3YheBufS0OM00mG9htc9i5gFdPp43t1P9ACg9AY
+gkHNZTfqjjJ+vWuZXTARyNtIVBw74acT02pIk/c9jH8F6M7ziCpjBLjqflh8AXtb
+4cV97yHgjQ5dUX2xZ/2jvTg2xvI4hocalmhgRvsoFEdV4aeADGvi6t9NfJBIoDa9
+CReJf8Py05yc493EG931t3GzUwWJBtDLSoDByFOQtTwxiBdQn8nEDovYqAJjDQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFLDCCBBSgAwIBAgIEOU99hzANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJX
+VzESMBAGA1UEChMJYmVUUlVTVGVkMRswGQYDVQQDExJiZVRSVVNUZWQgUm9vdCBD
+QXMxGjAYBgNVBAMTEWJlVFJVU1RlZCBSb290IENBMB4XDTAwMDYyMDE0MjEwNFoX
+DTEwMDYyMDEzMjEwNFowWjELMAkGA1UEBhMCV1cxEjAQBgNVBAoTCWJlVFJVU1Rl
+ZDEbMBkGA1UEAxMSYmVUUlVTVGVkIFJvb3QgQ0FzMRowGAYDVQQDExFiZVRSVVNU
+ZWQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANS0c3oT
+CjhVAb6JVuGUntS+WutKNHUbYSnE4a0IYCF4SP+00PpeQY1hRIfo7clY+vyTmt9P
+6j41ffgzeubx181vSUs9Ty1uDoM6GHh3o8/n9E1z2Jo7Gh2+lVPPIJfCzz4kUmwM
+jmVZxXH/YgmPqsWPzGCgc0rXOD8Vcr+il7dw6K/ifhYGTPWqZCZyByWtNfwYsSbX
+2P8ZDoMbjNx4RWc0PfSvHI3kbWvtILNnmrRhyxdviTX/507AMhLn7uzf/5cwdO2N
+R47rtMNE5qdMf1ZD6Li8tr76g5fmu/vEtpO+GRg+jIG5c4gW9JZDnGdzF5DYCW5j
+rEq2I8QBoa2k5MUCAwEAAaOCAfgwggH0MA8GA1UdEwEB/wQFMAMBAf8wggFZBgNV
+HSAEggFQMIIBTDCCAUgGCisGAQQBsT4BAAAwggE4MIIBAQYIKwYBBQUHAgIwgfQa
+gfFSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1
+bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0
+ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGFuZCBjZXJ0aWZpY2F0aW9uIHBy
+YWN0aWNlIHN0YXRlbWVudCwgd2hpY2ggY2FuIGJlIGZvdW5kIGF0IGJlVFJVU1Rl
+ZCdzIHdlYiBzaXRlLCBodHRwczovL3d3dy5iZVRSVVNUZWQuY29tL3ZhdWx0L3Rl
+cm1zMDEGCCsGAQUFBwIBFiVodHRwczovL3d3dy5iZVRSVVNUZWQuY29tL3ZhdWx0
+L3Rlcm1zMDQGA1UdHwQtMCswKaAnoCWkIzAhMRIwEAYDVQQKEwliZVRSVVNUZWQx
+CzAJBgNVBAYTAldXMB0GA1UdDgQWBBQquZtpLjub2M3eKjEENGvKBxirZzAfBgNV
+HSMEGDAWgBQquZtpLjub2M3eKjEENGvKBxirZzAOBgNVHQ8BAf8EBAMCAf4wDQYJ
+KoZIhvcNAQEFBQADggEBAHlh26Nebhax6nZR+csVm8tpvuaBa58oH2U+3RGFktTo
+Qb9+M70j5/Egv6S0phkBxoyNNXxlpE8JpNbYIxUFE6dDea/bow6be3ga8wSGWsb2
+jCBHOElQBp1yZzrwmAOtlmdE/D8QDYZN5AA7KXvOOzuZhmElQITcE2K3+spZ1gMe
+1lMBzW1MaFVA4e5rxyoAAEiCswoBw2AqDPeCNe5IhpbkdNQ96gFxugR1QKepfzk5
+mlWXKWWuGVUlBXJH0+gY3Ljpr0NzARJ0o+FcXxVdJPP55PS2Z2cS52QiivalQaYc
+tmBjRYoQtLpGEK5BV2VsPyMQPyEQWbfkQN0mDCP2qq4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGUTCCBTmgAwIBAgIEPLVPQDANBgkqhkiG9w0BAQUFADBmMRIwEAYDVQQKEwli
+ZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENBczEzMDEGA1UEAxMq
+YmVUUlVTVGVkIFJvb3QgQ0EgLSBFbnRydXN0IEltcGxlbWVudGF0aW9uMB4XDTAy
+MDQxMTA4MjQyN1oXDTIyMDQxMTA4NTQyN1owZjESMBAGA1UEChMJYmVUUlVTVGVk
+MRswGQYDVQQLExJiZVRSVVNUZWQgUm9vdCBDQXMxMzAxBgNVBAMTKmJlVFJVU1Rl
+ZCBSb290IENBIC0gRW50cnVzdCBJbXBsZW1lbnRhdGlvbjCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBALr0RAOqEmq1Q+xVkrYwfTVXDNvzDSduTPdQqJtO
+K2/b9a0cS12zqcH+e0TrW6MFDR/FNCswACnxeECypP869AGIF37m1CbTukzqMvtD
+d5eHI8XbQ6P1KqNRXuE70mVpflUVm3rnafdE4Fe1FehmYA8NA/uCjqPoEXtsvsdj
+DheT389Lrm5zdeDzqrmkwAkbhepxKYhBMvnwKg5sCfJ0a2ZsUhMfGLzUPvfYbiCe
+yv78IZTuEyhL11xeDGbu6bsPwTSxfwh28z0mcMmLJR1iJAzqHHVOwBLkuhMdMCkt
+VjMFu5dZfsZJT4nXLySotohAtWSSU1Yk5KKghbNekLQSM80CAwEAAaOCAwUwggMB
+MIIBtwYDVR0gBIIBrjCCAaowggGmBg8rBgEEAbE+AAACCSiDkTEwggGRMIIBSQYI
+KwYBBQUHAgIwggE7GoIBN1JlbGlhbmNlIG9uIG9yIHVzZSBvZiB0aGlzIENlcnRp
+ZmljYXRlIGNyZWF0ZXMgYW4gYWNrbm93bGVkZ21lbnQgYW5kIGFjY2VwdGFuY2Ug
+b2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0
+aW9ucyBvZiB1c2UsIHRoZSBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu
+dCBhbmQgdGhlIFJlbHlpbmcgUGFydHkgQWdyZWVtZW50LCB3aGljaCBjYW4gYmUg
+Zm91bmQgYXQgdGhlIGJlVFJVU1RlZCB3ZWIgc2l0ZSwgaHR0cHM6Ly93d3cuYmV0
+cnVzdGVkLmNvbS9wcm9kdWN0c19zZXJ2aWNlcy9pbmRleC5odG1sMEIGCCsGAQUF
+BwIBFjZodHRwczovL3d3dy5iZXRydXN0ZWQuY29tL3Byb2R1Y3RzX3NlcnZpY2Vz
+L2luZGV4Lmh0bWwwEQYJYIZIAYb4QgEBBAQDAgAHMIGJBgNVHR8EgYEwfzB9oHug
+eaR3MHUxEjAQBgNVBAoTCWJlVFJVU1RlZDEbMBkGA1UECxMSYmVUUlVTVGVkIFJv
+b3QgQ0FzMTMwMQYDVQQDEypiZVRSVVNUZWQgUm9vdCBDQSAtIEVudHJ1c3QgSW1w
+bGVtZW50YXRpb24xDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMjAwMjA0MTEw
+ODI0MjdagQ8yMDIyMDQxMTA4NTQyN1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaA
+FH1w5a44iwY/qhwaj/nPJDCqhIQWMB0GA1UdDgQWBBR9cOWuOIsGP6ocGo/5zyQw
+qoSEFjAMBgNVHRMEBTADAQH/MB0GCSqGSIb2fQdBAAQQMA4bCFY2LjA6NC4wAwIE
+kDANBgkqhkiG9w0BAQUFAAOCAQEAKrgXzh8QlOu4mre5X+za95IkrNySO8cgjfKZ
+5V04ocI07cUTWVwFtStPYZuR+0H8/NU8TZh2BvWBfevdkObRVlTa4y0MnxEylCIB
+evZsLHRnBMylj44ss0O1lKLQfelifwa+JwGDnjr9iu6YQ0pr17WXOzq/T220Y/oz
+ADQuLW2WyXvKmWO6vvT2MKAtmJbpVkQFqUSjYRDrgqFnXbxdJ3Wqiig2KjiS2d2k
+XgClzMx8KSreKJCrt+G2/30lC0DYqjSjLd4H61/OCt3Kfjp9JsFiaDrmLzfzgYYh
+xKlkqu9FNtEaZnz46TfW1mG+oq1I59/mdP7TbX3SJdysYlep9w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFaDCCBFCgAwIBAgIQO1nHe81bV569N1KsdrSqGjANBgkqhkiG9w0BAQUFADBi
+MRIwEAYDVQQKEwliZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENB
+czEvMC0GA1UEAxMmYmVUUlVTVGVkIFJvb3QgQ0EgLSBSU0EgSW1wbGVtZW50YXRp
+b24wHhcNMDIwNDExMTExODEzWhcNMjIwNDEyMTEwNzI1WjBiMRIwEAYDVQQKEwli
+ZVRSVVNUZWQxGzAZBgNVBAsTEmJlVFJVU1RlZCBSb290IENBczEvMC0GA1UEAxMm
+YmVUUlVTVGVkIFJvb3QgQ0EgLSBSU0EgSW1wbGVtZW50YXRpb24wggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkujQwCY5X0LkGLG9uJIAiv11DpvpPrILn
+HGhwhRujbrWqeNluB0s/6d/16uhUoWGKDi9pdRi3DOUUjXFumLhV/AyV0Jtu4S2I
+1DpAa5LxmZZk3tv/ePTulh1HiXzUvrmIdyM6CeYEnm2qXtLIvZpOGd+J6lsOfsPk
+tPDgaTuID0GQ+NRxQyTBjyZLO1bp/4xsN+lFrYWMU8NghpBKlsmzVLC7F/AcRdnU
+GxlkVgoZ98zh/4avflherHqQH8koOUV7orbHnB/ahdQhhlkwk75TMzf270HPM8er
+cmsl9fNTGwxMLvF1S++gh/f+ihXQbNXL+WhTuXAVE8L1LvtDNXUtAgMBAAGjggIY
+MIICFDAMBgNVHRMEBTADAQH/MIIBtQYDVR0gBIIBrDCCAagwggGkBg8rBgEEAbE+
+AAADCSiDkTEwggGPMEEGCCsGAQUFBwIBFjVodHRwOi8vd3d3LmJldHJ1c3RlZC5j
+b20vcHJvZHVjdHNfc2VydmljZXMvaW5kZXguaHRtbDCCAUgGCCsGAQUFBwICMIIB
+OhqCATZSZWxpYW5jZSBvbiBvciB1c2Ugb2YgdGhpcyBDZXJ0aWZpY2F0ZSBjcmVh
+dGVzIGFuIGFja25vd2xlZGdtZW50IGFuZCBhY2NlcHRhbmNlIG9mIHRoZSB0aGVu
+IGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNl
+LCB0aGUgQ2VydGlmaWNhdGlvbiBQcmFjdGljZSBTdGF0ZW1lbnQgYW5kIHRoZSBS
+ZWx5aW5nIFBhcnR5IEFncmVlbWVudCwgd2hpY2ggY2FuIGJlIGZvdW5kIGF0IHRo
+ZSBiZVRSVVNUZWQgd2ViIHNpdGUsIGh0dHA6Ly93d3cuYmV0cnVzdGVkLmNvbS9w
+cm9kdWN0c19zZXJ2aWNlcy9pbmRleC5odG1sMAsGA1UdDwQEAwIBBjAfBgNVHSME
+GDAWgBSp7BR++dlDzFMrFK3P9/BZiUHNGTAdBgNVHQ4EFgQUqewUfvnZQ8xTKxSt
+z/fwWYlBzRkwDQYJKoZIhvcNAQEFBQADggEBANuXsHXqDMTBmMpWBcCorSZIry0g
+6IHHtt9DwSwddUvUQo3neqh03GZCWYez9Wlt2ames30cMcH1VOJZJEnl7r05pmuK
+mET7m9cqg5c0Lcd9NUwtNLg+DcTsiCevnpL9UGGCqGAHFFPMZRPB9kdEadIxyKbd
+LrML3kqNWz2rDcI1UqJWN8wyiyiFQpyRQHpwKzg21eFzGh/l+n5f3NacOzDq28Bb
+J1zTcwfBwvNMm2+fG8oeqqg4MwlYsq78B+g23FW6L09A/nq9BqaBwZMifIYRCgZ3
+SK41ty8ymmFei74pnykkiFY5LKjSq5YDWtRIn7lAhAuYaPsBQ9Yb4gmxlxw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn
+MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL
+ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg
+b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa
+MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB
+ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw
+IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B
+AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb
+unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d
+BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq
+7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3
+0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX
+roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG
+A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j
+aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p
+26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA
+BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud
+EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN
+BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz
+aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB
+AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd
+p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi
+1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc
+XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0
+eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu
+tGWaIZDgqtCYvDi1czyL+Nw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn
+MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL
+ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo
+YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9
+MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy
+NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G
+A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA
+A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0
+Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s
+QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV
+eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795
+B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh
+z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T
+AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i
+ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w
+TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH
+MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD
+VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE
+VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh
+bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B
+AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM
+bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi
+ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG
+VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c
+ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/
+AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw
+PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz
+cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9
+MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz
+IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ
+ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR
+VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL
+kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd
+EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas
+H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0
+HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud
+DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4
+QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu
+Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/
+AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8
+yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
+FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA
+ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB
+kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
+l7+ijrRU
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM
+MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD
+QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM
+MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD
+QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E
+jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo
+ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI
+ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu
+Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg
+AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7
+HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA
+uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa
+TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg
+xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q
+CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x
+O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs
+6GAqm4VKQPNriiTsBhYscw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
+YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
+GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
+BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
+3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
+YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
+ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
+oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
+QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
+b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
+AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
+GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
+G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
+l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
+smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB
+gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
+BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
+MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl
+YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P
+RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3
+UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI
+2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8
+Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp
++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+
+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O
+nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW
+/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g
+PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u
+QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY
+SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv
+IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
+RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4
+zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd
+BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB
+ZQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
+IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw
+MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
+ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
+T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR
+FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J
+cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW
+BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm
+fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
+GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp
+ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow
+fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV
+BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM
+cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S
+HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996
+CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk
+3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz
+6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV
+HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
+EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv
+Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw
+Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww
+DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0
+5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj
+Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI
+gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ
+aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl
+izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0
+aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla
+MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD
+VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW
+fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt
+TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL
+fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW
+1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7
+kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G
+A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v
+ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo
+dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu
+Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/
+HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32
+pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS
+jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+
+xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn
+dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
+b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
+cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c
+JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP
+mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+
+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4
+VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/
+AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB
+AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun
+pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC
+dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf
+fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm
+NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx
+H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
+QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
+CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
+nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
+43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
+T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
+gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
+BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
+TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
+DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
+hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
+06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
+PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
+YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
+PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
+xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
+Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
+hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
+EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
+FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
+nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
+eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
+hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
+Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
++OkuE6N36B9K
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFijCCA3KgAwIBAgIQDHbanJEMTiye/hXQWJM8TDANBgkqhkiG9w0BAQUFADBf
+MQswCQYDVQQGEwJOTDESMBAGA1UEChMJRGlnaU5vdGFyMRowGAYDVQQDExFEaWdp
+Tm90YXIgUm9vdCBDQTEgMB4GCSqGSIb3DQEJARYRaW5mb0BkaWdpbm90YXIubmww
+HhcNMDcwNTE2MTcxOTM2WhcNMjUwMzMxMTgxOTIxWjBfMQswCQYDVQQGEwJOTDES
+MBAGA1UEChMJRGlnaU5vdGFyMRowGAYDVQQDExFEaWdpTm90YXIgUm9vdCBDQTEg
+MB4GCSqGSIb3DQEJARYRaW5mb0BkaWdpbm90YXIubmwwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQCssFjBAL3YIQgLK5r+blYwBZ8bd5AQQVzDDYcRd46B
+8cp86Yxq7Th0Nbva3/m7wAk3tJZzgX0zGpg595NvlX89ubF1h7pRSOiLcD6VBMXY
+tsMW2YiwsYcdcNqGtA8Ui3rPENF0NqISe3eGSnnme98CEWilToauNFibJBN4ViIl
+HgGLS1Fx+4LMWZZpiFpoU8W5DQI3y0u8ZkqQfioLBQftFl9VkHXYRskbg+IIvvEj
+zJkd1ioPgyAVWCeCLvriIsJJsbkBgWqdbZ1Ad2h2TiEqbYRAhU52mXyC8/O3AlnU
+JgEbjt+tUwbRrhjd4rI6y9eIOI6sWym5GdOY+RgDz0iChmYLG2kPyes4iHomGgVM
+ktck1JbyrFIto0fVUvY//s6EBnCmqj6i8rZWNBhXouSBbefK8GrTx5FrAoNBfBXv
+a5pkXuPQPOWx63tdhvvL5ndJzaNl3Pe5nLjkC1+Tz8wwGjIczhxjlaX56uF0i57p
+K6kwe6AYHw4YC+VbqdPRbB4HZ4+RS6mKvNJmqpMBiLKR+jFc1abBUggJzQpjotMi
+puih2TkGl/VujQKQjBR7P4DNG5y6xFhyI6+2Vp/GekIzKQc/gsnmHwUNzUwoNovT
+yD4cxojvXu6JZOkd69qJfjKmadHdzIif0dDJZiHcBmfFlHqabWJMfczgZICynkeO
+owIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV
+HQ4EFgQUiGi/4I41xDs4a2L3KDuEgcgM100wDQYJKoZIhvcNAQEFBQADggIBADsC
+jcs8MOhuoK3yc7NfniUTBAXT9uOLuwt5zlPe5JbF0a9zvNXD0EBVfEB/zRtfCdXy
+fJ9oHbtdzno5wozWmHvFg1Wo1X1AyuAe94leY12hE8JdiraKfADzI8PthV9xdvBo
+Y6pFITlIYXg23PFDk9Qlx/KAZeFTAnVR/Ho67zerhChXDNjU1JlWbOOi/lmEtDHo
+M/hklJRRl6s5xUvt2t2AC298KQ3EjopyDedTFLJgQT2EkTFoPSdE2+Xe9PpjRchM
+Ppj1P0G6Tss3DbpmmPHdy59c91Q2gmssvBNhl0L4eLvMyKKfyvBovWsdst+Nbwed
+2o5nx0ceyrm/KkKRt2NTZvFCo+H0Wk1Ya7XkpDOtXHAd3ODy63MUkZoDweoAZbwH
+/M8SESIsrqC9OuCiKthZ6SnTGDWkrBFfGbW1G/8iSlzGeuQX7yCpp/Q/rYqnmgQl
+nQ7KN+ZQ/YxCKQSa7LnPS3K94gg2ryMvYuXKAdNw23yCIywWMQzGNgeQerEfZ1jE
+O1hZibCMjFCz2IbLaKPECudpSyDOwR5WS5WpI2jYMNjD67BVUc3l/Su49bsRn1NU
+9jQZjHkJNsphFyUXC4KYcwx3dMPVDceoEkzHp1RxRy4sGn3J4ys7SN4nhKdjNrN9
+j6BkOSQNPXuHr2ZcdBtLc7LljPCGmbjlxd+Ewbfr
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV
+UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL
+EwhEU1RDQSBFMTAeFw05ODEyMTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJ
+BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x
+ETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCg
+bIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJENySZ
+j9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlV
+Sn5JTe2io74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCG
+SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx
+JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI
+RFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMTAxODEw
+MjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFGp5
+fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i
++DAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG
+SIb3DQEBBQUAA4GBACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lN
+QseSJqBcNJo4cvj9axY+IO6CizEqkzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+
+gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4RbyhkwS7hp86W0N6w4pl
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID2DCCAsACEQDQHkCLAAACfAAAAAIAAAABMA0GCSqGSIb3DQEBBQUAMIGpMQsw
+CQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMOU2FsdCBMYWtlIENp
+dHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UE
+CxMIRFNUQ0EgWDExFjAUBgNVBAMTDURTVCBSb290Q0EgWDExITAfBgkqhkiG9w0B
+CQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTAeFw05ODEyMDExODE4NTVaFw0wODExMjgx
+ODE4NTVaMIGpMQswCQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMO
+U2FsdCBMYWtlIENpdHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0
+IENvLjERMA8GA1UECxMIRFNUQ0EgWDExFjAUBgNVBAMTDURTVCBSb290Q0EgWDEx
+ITAfBgkqhkiG9w0BCQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBANLGJrbnpT3BxGjVUG9TxW9JEwm4ryxIjRRqoxdf
+WvnTLnUv2Chi0ZMv/E3Uq4flCMeZ55I/db3rJbQVwZsZPdJEjdd0IG03Ao9pk1uK
+xBmd9LIO/BZsubEFkoPRhSxglD5FVaDZqwgh5mDoO3TymVBRaNADLbGAvqPYUrBE
+zUNKcI5YhZXhTizWLUFv1oTnyJhEykfbLCSlaSbPa7gnYsP0yXqSI+0TZ4KuRS5F
+5X5yP4WdlGIQ5jyRoa13AOAV7POEgHJ6jm5gl8ckWRA0g1vhpaRptlc1HHhZxtMv
+OnNn7pTKBBMFYgZwI7P0fO5F2WQLW0mqpEPOJsREEmy43XkCAwEAATANBgkqhkiG
+9w0BAQUFAAOCAQEAojeyP2n714Z5VEkxlTMr89EJFEliYIalsBHiUMIdBlc+Legz
+ZL6bqq1fG03UmZWii5rJYnK1aerZWKs17RWiQ9a2vAd5ZWRzfdd5ynvVWlHG4VME
+lo04z6MXrDlxawHDi1M8Y+nuecDkvpIyZHqzH5eUYr3qsiAVlfuX8ngvYzZAOONG
+Dx3drJXK50uQe7FLqdTF65raqtWjlBRGjS0f8zrWkzr2Pnn86Oawde3uPclwx12q
+gUtGJRzHbBXjlU4PqjI3lAoXJJIThFjSY28r9+ZbYgsTF7ANUkz+/m9c4pFuHf2k
+Ytdo+o56T9II2pPc8JIRetDccpMMc5NihWjQ9A==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV
+UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL
+EwhEU1RDQSBFMjAeFw05ODEyMDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJ
+BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x
+ETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC/
+k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGODVvso
+LeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3o
+TQPMx7JSxhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCG
+SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx
+JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI
+RFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMDkxOTE3
+MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFB6C
+TShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5
+WzAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG
+SIb3DQEBBQUAA4GBAEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHR
+xdf0CiUPPXiBng+xZ8SQTGPdXqfiup/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVL
+B3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1mPnHfxsb1gYgAlihw6ID
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID2DCCAsACEQDQHkCLAAB3bQAAAAEAAAAEMA0GCSqGSIb3DQEBBQUAMIGpMQsw
+CQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMOU2FsdCBMYWtlIENp
+dHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UE
+CxMIRFNUQ0EgWDIxFjAUBgNVBAMTDURTVCBSb290Q0EgWDIxITAfBgkqhkiG9w0B
+CQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTAeFw05ODExMzAyMjQ2MTZaFw0wODExMjcy
+MjQ2MTZaMIGpMQswCQYDVQQGEwJ1czENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMO
+U2FsdCBMYWtlIENpdHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0
+IENvLjERMA8GA1UECxMIRFNUQ0EgWDIxFjAUBgNVBAMTDURTVCBSb290Q0EgWDIx
+ITAfBgkqhkiG9w0BCQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBANx18IzAdZaawGIfJvfE4Zrq4FZzW5nNAUSoCLbV
+p9oaBBg5kkp4o4HC9Xd6ULRw/5qrxsfKboNPQpj7Jgva3G3WqZlVUmfpKAOS3OWw
+BZoPFflrWXJW8vo5/Kpo7g8fEIMv/J36F5bdguPmRX3AS4BEH+0s4IT9kVySVGkl
+5WJp3OXuAFK9MwutdQKFp2RQLcUZGTDAJtvJ0/0uma1ZtQtN1EGuhUhDWdy3qOKi
+3sOP17ihYqZoUFLkzzGnlIXan0YyF1bl8utmPRL/Q9uY73fPy4GNNLHGUEom0eQ+
+QVCvbK4iNC7Va26Dunm4dmVI2gkpZGMiuftHdoWMhkTLCdsCAwEAATANBgkqhkiG
+9w0BAQUFAAOCAQEAtTYOXeFhKFoRZcA/gwN5Tb4opgsHAlKFzfiR0BBstWogWxyQ
+2TA8xkieil5k+aFxd+8EJx8H6+Qm93N0yUQYGmbT4EOvkTvRyyzYdFQ6HE3K1GjN
+I3wdEJ5F6fYAbqbNGf9PLCmPV03Ed5K+4EwJ+11EhmYhqLkyolbV6YyDfFk/xPEL
+553snr2cGA4+wjl5KLcDDQjLxufZATdQEOzMYRZA1K8xdHv8PzGn0EdzMzkbzE5q
+10mDEQb+64JYMzJM8FasHpwvVpp7wUocpf1VNs78lk30sPDst2yC7S8xmUJMqbIN
+uBVd8d+6ybVK1GSYsyapMMj9puyrliGtf8J4tg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx
+ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w
+MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD
+VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx
+FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu
+ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7
+gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH
+fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a
+ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT
+ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF
+MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk
+c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto
+dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt
+aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI
+hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk
+QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/
+h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq
+nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR
+rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2
+9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
+Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
+rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
+OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
+xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
+7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
+aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
+SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
+ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
+AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
+R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
+JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEgzCCA+ygAwIBAgIEOJ725DANBgkqhkiG9w0BAQQFADCBtDEUMBIGA1UEChML
+RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9HQ0NBX0NQUyBp
+bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAyMDAw
+IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENsaWVu
+dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMDAyMDcxNjE2NDBaFw0yMDAy
+MDcxNjQ2NDBaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3
+LmVudHJ1c3QubmV0L0dDQ0FfQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp
+YWIuKTElMCMGA1UECxMcKGMpIDIwMDAgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG
+A1UEAxMqRW50cnVzdC5uZXQgQ2xpZW50IENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCTdLS25MVL1qFof2LV7PdRV7Ny
+Spj10InJrWPNTTVRaoTUrcloeW+46xHbh65cJFET8VQlhK8pK5/jgOLZy93GRUk0
+iJBeAZfv6lOm3fzB3ksqJeTpNfpVBQbliXrqpBFXO/x8PTbNZzVtpKklWb1m9fkn
+5JVn1j+SgF7yNH0rhQIDAQABo4IBnjCCAZowEQYJYIZIAYb4QgEBBAQDAgAHMIHd
+BgNVHR8EgdUwgdIwgc+ggcyggcmkgcYwgcMxFDASBgNVBAoTC0VudHJ1c3QubmV0
+MUAwPgYDVQQLFDd3d3cuZW50cnVzdC5uZXQvR0NDQV9DUFMgaW5jb3JwLiBieSBy
+ZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMjAwMCBFbnRydXN0Lm5l
+dCBMaW1pdGVkMTMwMQYDVQQDEypFbnRydXN0Lm5ldCBDbGllbnQgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMjAwMDAy
+MDcxNjE2NDBagQ8yMDIwMDIwNzE2NDY0MFowCwYDVR0PBAQDAgEGMB8GA1UdIwQY
+MBaAFISLdP3FjcD/J20gN0V8/i3OutN9MB0GA1UdDgQWBBSEi3T9xY3A/ydtIDdF
+fP4tzrrTfTAMBgNVHRMEBTADAQH/MB0GCSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4w
+AwIEkDANBgkqhkiG9w0BAQQFAAOBgQBObzWAO9GK9Q6nIMstZVXQkvTnhLUGJoMS
+hAusO7JE7r3PQNsgDrpuFOow4DtifH+La3xKp9U1PL6oXOpLu5OOgGarDyn9TS2/
+GpsKkMWr2tGzhtQvJFJcem3G8v7lTRowjJDyutdKPkN+1MhQGof4T4HHdguEOnKd
+zmVml64mXg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIElTCCA/6gAwIBAgIEOJsRPDANBgkqhkiG9w0BAQQFADCBujEUMBIGA1UEChML
+RW50cnVzdC5uZXQxPzA9BgNVBAsUNnd3dy5lbnRydXN0Lm5ldC9TU0xfQ1BTIGlu
+Y29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDIwMDAg
+RW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJl
+IFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMDAyMDQxNzIwMDBa
+Fw0yMDAyMDQxNzUwMDBaMIG6MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDE/MD0GA1UE
+CxQ2d3d3LmVudHJ1c3QubmV0L1NTTF9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p
+dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMjAwMCBFbnRydXN0Lm5ldCBMaW1pdGVk
+MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp
+b24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHwV9OcfHO
+8GCGD9JYf9Mzly0XonUwtZZkJi9ow0SrqHXmAGc0V55lxyKbc+bT3QgON1WqJUaB
+bL3+qPZ1V1eMkGxKwz6LS0MKyRFWmponIpnPVZ5h2QLifLZ8OAfc439PmrkDQYC2
+dWcTC5/oVzbIXQA23mYU2m52H083jIITiQIDAQABo4IBpDCCAaAwEQYJYIZIAYb4
+QgEBBAQDAgAHMIHjBgNVHR8EgdswgdgwgdWggdKggc+kgcwwgckxFDASBgNVBAoT
+C0VudHJ1c3QubmV0MT8wPQYDVQQLFDZ3d3cuZW50cnVzdC5uZXQvU1NMX0NQUyBp
+bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAyMDAw
+IEVudHJ1c3QubmV0IExpbWl0ZWQxOjA4BgNVBAMTMUVudHJ1c3QubmV0IFNlY3Vy
+ZSBTZXJ2ZXIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxDTALBgNVBAMTBENSTDEw
+KwYDVR0QBCQwIoAPMjAwMDAyMDQxNzIwMDBagQ8yMDIwMDIwNDE3NTAwMFowCwYD
+VR0PBAQDAgEGMB8GA1UdIwQYMBaAFMtswGvjuz7L/CKc/vuLkpyw8m4iMB0GA1Ud
+DgQWBBTLbMBr47s+y/winP77i5KcsPJuIjAMBgNVHRMEBTADAQH/MB0GCSqGSIb2
+fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0BAQQFAAOBgQBi24GRzsia
+d0Iv7L0no1MPUBvqTpLwqa+poLpIYcvvyQbvH9X07t9WLebKahlzqlO+krNQAraF
+JnJj2HVQYnUUt7NQGj/KEQALhUVpbbalrlHhStyCP2yMNLJ3a9kC9n8O6mUE8c1U
+yrrJzOCE98g+EZfTYAkYvAX/bIkz8OwVDw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML
+RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp
+bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5
+IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0xOTEy
+MjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3
+LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp
+YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG
+A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq
+K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe
+sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX
+MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT
+XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/
+HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH
+4QIDAQABo3QwcjARBglghkgBhvhCAQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGA
+vtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdERgL7YibkIozH5oSQJFrlwMB0G
+CSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEA
+WUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo
+oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQ
+h7A6tcOdBTcSo8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18
+f3v/rxzP5tsHrV7bhZ3QKw0z2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfN
+B/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjXOP/swNlQ8C5LWK5Gb9Auw2DaclVy
+vUxFnmG6v4SBkgPR0ml8xQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIE7TCCBFagAwIBAgIEOAOR7jANBgkqhkiG9w0BAQQFADCByTELMAkGA1UEBhMC
+VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MUgwRgYDVQQLFD93d3cuZW50cnVzdC5u
+ZXQvQ2xpZW50X0NBX0luZm8vQ1BTIGluY29ycC4gYnkgcmVmLiBsaW1pdHMgbGlh
+Yi4xJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV
+BAMTKkVudHJ1c3QubmV0IENsaWVudCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
+Fw05OTEwMTIxOTI0MzBaFw0xOTEwMTIxOTU0MzBaMIHJMQswCQYDVQQGEwJVUzEU
+MBIGA1UEChMLRW50cnVzdC5uZXQxSDBGBgNVBAsUP3d3dy5lbnRydXN0Lm5ldC9D
+bGllbnRfQ0FfSW5mby9DUFMgaW5jb3JwLiBieSByZWYuIGxpbWl0cyBsaWFiLjEl
+MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMq
+RW50cnVzdC5uZXQgQ2xpZW50IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0G
+CSqGSIb3DQEBAQUAA4GLADCBhwKBgQDIOpleMRffrCdvkHvkGf9FozTC28GoT/Bo
+6oT9n3V5z8GKUZSvx1cDR2SerYIbWtp/N3hHuzeYEpbOxhN979IMMFGpOZ5V+Pux
+5zDeg7K6PvHViTs7hbqqdCz+PzFur5GVbgbUB01LLFZHGARS2g4Qk79jkJvh34zm
+AqTmT173iwIBA6OCAeAwggHcMBEGCWCGSAGG+EIBAQQEAwIABzCCASIGA1UdHwSC
+ARkwggEVMIHkoIHhoIHepIHbMIHYMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50
+cnVzdC5uZXQxSDBGBgNVBAsUP3d3dy5lbnRydXN0Lm5ldC9DbGllbnRfQ0FfSW5m
+by9DUFMgaW5jb3JwLiBieSByZWYuIGxpbWl0cyBsaWFiLjElMCMGA1UECxMcKGMp
+IDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5uZXQg
+Q2xpZW50IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCyg
+KqAohiZodHRwOi8vd3d3LmVudHJ1c3QubmV0L0NSTC9DbGllbnQxLmNybDArBgNV
+HRAEJDAigA8xOTk5MTAxMjE5MjQzMFqBDzIwMTkxMDEyMTkyNDMwWjALBgNVHQ8E
+BAMCAQYwHwYDVR0jBBgwFoAUxPucKXuXzUyW/O5bs8qZdIuV6kwwHQYDVR0OBBYE
+FMT7nCl7l81MlvzuW7PKmXSLlepMMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA
+BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEEBQADgYEAP66K8ddmAwWePvrqHEa7
+pFuPeJoSSJn59DXeDDYHAmsQOokUgZwxpnyyQbJq5wcBoUv5nyU7lsqZwz6hURzz
+wy5E97BnRqqS5TvaHBkUODDV4qIxJS7x7EU47fgGWANzYrAQMY9Av2TgXD7FTx/a
+EkP/TOYGJqibGapEPHayXOw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC
+VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u
+ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc
+KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u
+ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1
+MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE
+ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j
+b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF
+bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg
+U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA
+A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/
+I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3
+wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC
+AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb
+oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5
+BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p
+dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk
+MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp
+b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu
+dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0
+MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi
+E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa
+MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI
+hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN
+95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd
+2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC
+VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0
+Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW
+KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl
+cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw
+NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw
+NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy
+ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV
+BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo
+Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4
+4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9
+KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI
+rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi
+94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB
+sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi
+gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo
+kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE
+vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t
+O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua
+AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP
+9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/
+eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
+0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
+dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
+MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx
+dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f
+BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A
+cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC
+AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ
+MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm
+aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
+ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
+IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
+MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
+A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
+7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
+1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT
+ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw
+MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j
+LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ
+KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo
+RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu
+WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw
+Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD
+AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK
+eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM
+zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+
+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN
+/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
+UzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj
+dXJlIGVCdXNpbmVzcyBDQS0yMB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0
+NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD
+VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G
+vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/
+BPO3QSQ5BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0C
+AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEX
+MBUGA1UEChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJl
+IGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTkw
+NjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9euSBIplBq
+y/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQF
+MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
+A4GBAAyGgq3oThr1jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy
+0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1
+E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUmV+GRMOrN
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT
+ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw
+MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj
+dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l
+c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC
+UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc
+58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/
+o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH
+MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr
+aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA
+A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA
+Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv
+8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEVzCCAz+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMCRVMx
+IjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1
+dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2
+MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20w
+HhcNMDExMDI0MjIwMDAwWhcNMTMxMDI0MjIwMDAwWjCBnTELMAkGA1UEBhMCRVMx
+IjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1
+dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2
+MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20w
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDnIwNvbyOlXnjOlSztlB5u
+Cp4Bx+ow0Syd3Tfom5h5VtP8c9/Qit5Vj1H5WuretXDE7aTt/6MNbg9kUDGvASdY
+rv5sp0ovFy3Tc9UTHI9ZpTQsHVQERc1ouKDAA6XPhUJHlShbz++AbOCQl4oBPB3z
+hxAwJkh91/zpnZFx/0GaqUC1N5wpIE8fUuOgfRNtVLcK3ulqTgesrBlf3H5idPay
+BQC6haD9HThuy1q7hryUZzM1gywfI834yJFxzJeL764P3CkDG8A563DtwW4O2GcL
+iam8NeTvtjS0pbbELaW+0MOUJEjb35bTALVmGotmBQ/dPz/LP6pemkr4tErvlTcb
+AgMBAAGjgZ8wgZwwKgYDVR0RBCMwIYYfaHR0cDovL3d3dy5maXJtYXByb2Zlc2lv
+bmFsLmNvbTASBgNVHRMBAf8ECDAGAQH/AgEBMCsGA1UdEAQkMCKADzIwMDExMDI0
+MjIwMDAwWoEPMjAxMzEwMjQyMjAwMDBaMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E
+FgQUMwugZtHq2s7eYpMEKFK1FH84aLcwDQYJKoZIhvcNAQEFBQADggEBAEdz/o0n
+VPD11HecJ3lXV7cVVuzH2Fi3AQL0M+2TUIiefEaxvT8Ub/GzR0iLjJcG1+p+o1wq
+u00vR+L4OQbJnC4xGgN49Lw4xiKLMzHwFgQEffl25EvXwOaD7FnMP97/T2u3Z36m
+hoEyIwOdyPdfwUpgpZKpsaSgYMN4h7Mi8yrrW6ntBas3D7Hi05V2Y1Z0jFhyGzfl
+ZKG+TQyTmAyX9odtsz/ny4Cm7YjHX1BiAuiZdBbQ5rQ58SfLyEDW44YQqSMSkuBp
+QWOnryULwMWSyx6Yo1q6xTMPoJcB3X/ge9YGVM+h4k0460tQtcsm9MracEpqoeJ5
+quGnM/b9Sh/22WA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs
+IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG
+EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg
+R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A
+PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8
+Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL
+TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL
+5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7
+S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe
+2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
+FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap
+EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td
+EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv
+/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN
+A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0
+abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF
+I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz
+4iIprn2DQKi6bA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
+MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
+YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
+EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
+R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
+9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
+fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
+iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
+1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
+MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
+ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
+uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
+Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
+tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
+PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
+hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
+5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY
+MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo
+R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx
+MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK
+Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9
+AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA
+ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0
+7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W
+kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G
+A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ
+KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1
+6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl
+4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K
+oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj
+UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU
+AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy
+c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD
+VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1
+c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81
+WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG
+FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq
+XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL
+se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb
+KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd
+IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73
+y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt
+hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc
+QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4
+Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV
+HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ
+KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z
+dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ
+L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr
+Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo
+ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY
+T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz
+GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m
+1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV
+OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH
+6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX
+QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy
+c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE
+BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0
+IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV
+VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8
+cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT
+QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh
+F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v
+c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w
+mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd
+VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX
+teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ
+f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe
+Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+
+nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB
+/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY
+MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG
+9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc
+aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX
+IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn
+ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z
+uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN
+Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja
+QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW
+koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9
+ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt
+DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm
+bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
+A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
+b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
+MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
+YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
+aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
+jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
+xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
+1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
+snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
+U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
+9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
+BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
+AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
+yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
+38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
+AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
+DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
+HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
+MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
+v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
+eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
+tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
+C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
+zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
+mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
+V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
+bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
+3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
+J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
+291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
+ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
+AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
+TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh
+MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE
+YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3
+MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo
+ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg
+MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN
+ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA
+PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w
+wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi
+EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY
+avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+
+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE
+sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h
+/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5
+IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
+ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy
+OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P
+TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
+HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER
+dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf
+ReYNnyicsbkqWletNw+vHX/bvZ8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD
+VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv
+bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv
+b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV
+UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU
+cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds
+b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH
+iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS
+r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4
+04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r
+GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9
+3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P
+lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIB+jCCAWMCAgGjMA0GCSqGSIb3DQEBBAUAMEUxCzAJBgNVBAYTAlVTMRgwFgYD
+VQQKEw9HVEUgQ29ycG9yYXRpb24xHDAaBgNVBAMTE0dURSBDeWJlclRydXN0IFJv
+b3QwHhcNOTYwMjIzMjMwMTAwWhcNMDYwMjIzMjM1OTAwWjBFMQswCQYDVQQGEwJV
+UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMRwwGgYDVQQDExNHVEUgQ3liZXJU
+cnVzdCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC45k+625h8cXyv
+RLfTD0bZZOWTwUKOx7pJjTUteueLveUFMVnGsS8KDPufpz+iCWaEVh43KRuH6X4M
+ypqfpX/1FZSj1aJGgthoTNE3FQZor734sLPwKfWVWgkWYXcKIiXUT0Wqx73llt/5
+1KiOQswkwB6RJ0q1bQaAYznEol44AwIDAQABMA0GCSqGSIb3DQEBBAUAA4GBABKz
+dcZfHeFhVYAA1IFLezEPI2PnPfMD+fQ2qLvZ46WXTeorKeDWanOB5sCJo9Px4KWl
+IjeaY8JIILTbcuPI9tl8vrGvU9oUtCG41tWW4/5ODFlitppK+ULdjG+BqXH/9Apy
+bW1EDp3zdHSo1TRJ6V6e6bR64eVaH4QwnNOfpSXY
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIH9zCCB2CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARwxCzAJBgNVBAYTAkVT
+MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE
+ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE
+ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEzMDEGA1UECxMq
+SVBTIENBIENoYWluZWQgQ0FzIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYD
+VQQDEypJUFMgQ0EgQ2hhaW5lZCBDQXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
+HjAcBgkqhkiG9w0BCQEWD2lwc0BtYWlsLmlwcy5lczAeFw0wMTEyMjkwMDUzNTha
+Fw0yNTEyMjcwMDUzNThaMIIBHDELMAkGA1UEBhMCRVMxEjAQBgNVBAgTCUJhcmNl
+bG9uYTESMBAGA1UEBxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJUFMgSW50ZXJuZXQg
+cHVibGlzaGluZyBTZXJ2aWNlcyBzLmwuMSswKQYDVQQKFCJpcHNAbWFpbC5pcHMu
+ZXMgQy5JLkYuICBCLTYwOTI5NDUyMTMwMQYDVQQLEypJUFMgQ0EgQ2hhaW5lZCBD
+QXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMzAxBgNVBAMTKklQUyBDQSBDaGFp
+bmVkIENBcyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEeMBwGCSqGSIb3DQEJARYP
+aXBzQG1haWwuaXBzLmVzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcVpJJ
+spQgvJhPUOtopKdJC7/SMejHT8KGC/po/UNaivNgkjWZOLtNA1IhW/A3mTXhQSCB
+hYEFcYGdtJUZqV92NC5jNzVXjrQfQj8VXOF6wV8TGDIxya2+o8eDZh65nAQTy2nB
+Bt4wBrszo7Uf8I9vzv+W6FS+ZoCua9tBhDaiPQIDAQABo4IEQzCCBD8wHQYDVR0O
+BBYEFKGtMbH5PuEXpsirNPxShwkeYlJBMIIBTgYDVR0jBIIBRTCCAUGAFKGtMbH5
+PuEXpsirNPxShwkeYlJBoYIBJKSCASAwggEcMQswCQYDVQQGEwJFUzESMBAGA1UE
+CBMJQmFyY2Vsb25hMRIwEAYDVQQHEwlCYXJjZWxvbmExLjAsBgNVBAoTJUlQUyBJ
+bnRlcm5ldCBwdWJsaXNoaW5nIFNlcnZpY2VzIHMubC4xKzApBgNVBAoUImlwc0Bt
+YWlsLmlwcy5lcyBDLkkuRi4gIEItNjA5Mjk0NTIxMzAxBgNVBAsTKklQUyBDQSBD
+aGFpbmVkIENBcyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAxMqSVBT
+IENBIENoYWluZWQgQ0FzIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR4wHAYJKoZI
+hvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOCAQAwDAYDVR0TBAUwAwEB/zAMBgNVHQ8E
+BQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMG
+CCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCNwIBFQYKKwYBBAGCNwIBFgYKKwYB
+BAGCNwoDAQYKKwYBBAGCNwoDBDARBglghkgBhvhCAQEEBAMCAAcwGgYDVR0RBBMw
+EYEPaXBzQG1haWwuaXBzLmVzMBoGA1UdEgQTMBGBD2lwc0BtYWlsLmlwcy5lczBC
+BglghkgBhvhCAQ0ENRYzQ2hhaW5lZCBDQSBDZXJ0aWZpY2F0ZSBpc3N1ZWQgYnkg
+aHR0cDovL3d3dy5pcHMuZXMvMCkGCWCGSAGG+EIBAgQcFhpodHRwOi8vd3d3Lmlw
+cy5lcy9pcHMyMDAyLzA3BglghkgBhvhCAQQEKhYoaHR0cDovL3d3dy5pcHMuZXMv
+aXBzMjAwMi9pcHMyMDAyQ0FDLmNybDA8BglghkgBhvhCAQMELxYtaHR0cDovL3d3
+dy5pcHMuZXMvaXBzMjAwMi9yZXZvY2F0aW9uQ0FDLmh0bWw/MDkGCWCGSAGG+EIB
+BwQsFipodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL3JlbmV3YWxDQUMuaHRtbD8w
+NwYJYIZIAYb4QgEIBCoWKGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcG9saWN5
+Q0FDLmh0bWwwbQYDVR0fBGYwZDAuoCygKoYoaHR0cDovL3d3dy5pcHMuZXMvaXBz
+MjAwMi9pcHMyMDAyQ0FDLmNybDAyoDCgLoYsaHR0cDovL3d3d2JhY2suaXBzLmVz
+L2lwczIwMDIvaXBzMjAwMkNBQy5jcmwwLwYIKwYBBQUHAQEEIzAhMB8GCCsGAQUF
+BzABhhNodHRwOi8vb2NzcC5pcHMuZXMvMA0GCSqGSIb3DQEBBQUAA4GBAERyMJ1W
+WKJBGyi3leGmGpVfp3hAK+/blkr8THFj2XOVvQLiogbHvpcqk4A0hgP63Ng9HgfN
+HnNDJGD1HWHc3JagvPsd4+cSACczAsDAK1M92GsDgaPb1pOVIO/Tln4mkImcJpvN
+b2ar7QMiRDjMWb2f2/YHogF/JsRj9SVCXmK9
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIH6jCCB1OgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARIxCzAJBgNVBAYTAkVT
+MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE
+ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE
+ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEuMCwGA1UECxMl
+SVBTIENBIENMQVNFMSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMl
+SVBTIENBIENMQVNFMSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEeMBwGCSqGSIb3
+DQEJARYPaXBzQG1haWwuaXBzLmVzMB4XDTAxMTIyOTAwNTkzOFoXDTI1MTIyNzAw
+NTkzOFowggESMQswCQYDVQQGEwJFUzESMBAGA1UECBMJQmFyY2Vsb25hMRIwEAYD
+VQQHEwlCYXJjZWxvbmExLjAsBgNVBAoTJUlQUyBJbnRlcm5ldCBwdWJsaXNoaW5n
+IFNlcnZpY2VzIHMubC4xKzApBgNVBAoUImlwc0BtYWlsLmlwcy5lcyBDLkkuRi4g
+IEItNjA5Mjk0NTIxLjAsBgNVBAsTJUlQUyBDQSBDTEFTRTEgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkxLjAsBgNVBAMTJUlQUyBDQSBDTEFTRTEgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkxHjAcBgkqhkiG9w0BCQEWD2lwc0BtYWlsLmlwcy5lczCBnzAN
+BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA4FEnpwvdr9G5Q1uCN0VWcu+atsIS7ywS
+zHb5BlmvXSHU0lq4oNTzav3KaY1mSPd05u42veiWkXWmcSjK5yISMmmwPh5r9FBS
+YmL9Yzt9fuzuOOpi9GyocY3h6YvJP8a1zZRCb92CRTzo3wno7wpVqVZHYUxJZHMQ
+KD/Kvwn/xi8CAwEAAaOCBEowggRGMB0GA1UdDgQWBBTrsxl588GlHKzcuh9morKb
+adB4CDCCAUQGA1UdIwSCATswggE3gBTrsxl588GlHKzcuh9morKbadB4CKGCARqk
+ggEWMIIBEjELMAkGA1UEBhMCRVMxEjAQBgNVBAgTCUJhcmNlbG9uYTESMBAGA1UE
+BxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJUFMgSW50ZXJuZXQgcHVibGlzaGluZyBT
+ZXJ2aWNlcyBzLmwuMSswKQYDVQQKFCJpcHNAbWFpbC5pcHMuZXMgQy5JLkYuICBC
+LTYwOTI5NDUyMS4wLAYDVQQLEyVJUFMgQ0EgQ0xBU0UxIENlcnRpZmljYXRpb24g
+QXV0aG9yaXR5MS4wLAYDVQQDEyVJUFMgQ0EgQ0xBU0UxIENlcnRpZmljYXRpb24g
+QXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOCAQAwDAYD
+VR0TBAUwAwEB/zAMBgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUFBwMBBggr
+BgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCNwIB
+FQYKKwYBBAGCNwIBFgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglghkgBhvhC
+AQEEBAMCAAcwGgYDVR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1UdEgQTMBGB
+D2lwc0BtYWlsLmlwcy5lczBBBglghkgBhvhCAQ0ENBYyQ0xBU0UxIENBIENlcnRp
+ZmljYXRlIGlzc3VlZCBieSBodHRwOi8vd3d3Lmlwcy5lcy8wKQYJYIZIAYb4QgEC
+BBwWGmh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvMDoGCWCGSAGG+EIBBAQtFito
+dHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTEuY3JsMD8GCWCG
+SAGG+EIBAwQyFjBodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL3Jldm9jYXRpb25D
+TEFTRTEuaHRtbD8wPAYJYIZIAYb4QgEHBC8WLWh0dHA6Ly93d3cuaXBzLmVzL2lw
+czIwMDIvcmVuZXdhbENMQVNFMS5odG1sPzA6BglghkgBhvhCAQgELRYraHR0cDov
+L3d3dy5pcHMuZXMvaXBzMjAwMi9wb2xpY3lDTEFTRTEuaHRtbDBzBgNVHR8EbDBq
+MDGgL6AthitodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTEu
+Y3JsMDWgM6Axhi9odHRwOi8vd3d3YmFjay5pcHMuZXMvaXBzMjAwMi9pcHMyMDAy
+Q0xBU0UxLmNybDAvBggrBgEFBQcBAQQjMCEwHwYIKwYBBQUHMAGGE2h0dHA6Ly9v
+Y3NwLmlwcy5lcy8wDQYJKoZIhvcNAQEFBQADgYEAK9Dr/drIyllq2tPMMi7JVBuK
+Yn4VLenZMdMu9Ccj/1urxUq2ckCuU3T0vAW0xtnIyXf7t/k0f3gA+Nak5FI/LEpj
+V4F1Wo7ojPsCwJTGKbqz3Bzosq/SLmJbGqmODszFV0VRFOlOHIilkfSj945RyKm+
+hjM+5i9Ibq9UkE6tsSU=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIH6jCCB1OgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARIxCzAJBgNVBAYTAkVT
+MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE
+ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE
+ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEuMCwGA1UECxMl
+SVBTIENBIENMQVNFMyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMl
+SVBTIENBIENMQVNFMyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEeMBwGCSqGSIb3
+DQEJARYPaXBzQG1haWwuaXBzLmVzMB4XDTAxMTIyOTAxMDE0NFoXDTI1MTIyNzAx
+MDE0NFowggESMQswCQYDVQQGEwJFUzESMBAGA1UECBMJQmFyY2Vsb25hMRIwEAYD
+VQQHEwlCYXJjZWxvbmExLjAsBgNVBAoTJUlQUyBJbnRlcm5ldCBwdWJsaXNoaW5n
+IFNlcnZpY2VzIHMubC4xKzApBgNVBAoUImlwc0BtYWlsLmlwcy5lcyBDLkkuRi4g
+IEItNjA5Mjk0NTIxLjAsBgNVBAsTJUlQUyBDQSBDTEFTRTMgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkxLjAsBgNVBAMTJUlQUyBDQSBDTEFTRTMgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkxHjAcBgkqhkiG9w0BCQEWD2lwc0BtYWlsLmlwcy5lczCBnzAN
+BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqxf+DrDGaBtT8FK+n/ra+osTBLsBjzLZ
+H49NzjaY2uQARIwo2BNEKqRrThckQpzTiKRBgtYj+4vJhuW5qYIF3PHeH+AMmVWY
+8jjsbJ0gA8DvqqPGZARRLXgNo9KoOtYkTOmWehisEyMiG3zoMRGzXwmqMHBxRiVr
+SXGAK5UBsh8CAwEAAaOCBEowggRGMB0GA1UdDgQWBBS4k/8uy9wsjqLnev42USGj
+mFsMNDCCAUQGA1UdIwSCATswggE3gBS4k/8uy9wsjqLnev42USGjmFsMNKGCARqk
+ggEWMIIBEjELMAkGA1UEBhMCRVMxEjAQBgNVBAgTCUJhcmNlbG9uYTESMBAGA1UE
+BxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJUFMgSW50ZXJuZXQgcHVibGlzaGluZyBT
+ZXJ2aWNlcyBzLmwuMSswKQYDVQQKFCJpcHNAbWFpbC5pcHMuZXMgQy5JLkYuICBC
+LTYwOTI5NDUyMS4wLAYDVQQLEyVJUFMgQ0EgQ0xBU0UzIENlcnRpZmljYXRpb24g
+QXV0aG9yaXR5MS4wLAYDVQQDEyVJUFMgQ0EgQ0xBU0UzIENlcnRpZmljYXRpb24g
+QXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOCAQAwDAYD
+VR0TBAUwAwEB/zAMBgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUFBwMBBggr
+BgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYBBAGCNwIB
+FQYKKwYBBAGCNwIBFgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglghkgBhvhC
+AQEEBAMCAAcwGgYDVR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1UdEgQTMBGB
+D2lwc0BtYWlsLmlwcy5lczBBBglghkgBhvhCAQ0ENBYyQ0xBU0UzIENBIENlcnRp
+ZmljYXRlIGlzc3VlZCBieSBodHRwOi8vd3d3Lmlwcy5lcy8wKQYJYIZIAYb4QgEC
+BBwWGmh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvMDoGCWCGSAGG+EIBBAQtFito
+dHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTMuY3JsMD8GCWCG
+SAGG+EIBAwQyFjBodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL3Jldm9jYXRpb25D
+TEFTRTMuaHRtbD8wPAYJYIZIAYb4QgEHBC8WLWh0dHA6Ly93d3cuaXBzLmVzL2lw
+czIwMDIvcmVuZXdhbENMQVNFMy5odG1sPzA6BglghkgBhvhCAQgELRYraHR0cDov
+L3d3dy5pcHMuZXMvaXBzMjAwMi9wb2xpY3lDTEFTRTMuaHRtbDBzBgNVHR8EbDBq
+MDGgL6AthitodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyL2lwczIwMDJDTEFTRTMu
+Y3JsMDWgM6Axhi9odHRwOi8vd3d3YmFjay5pcHMuZXMvaXBzMjAwMi9pcHMyMDAy
+Q0xBU0UzLmNybDAvBggrBgEFBQcBAQQjMCEwHwYIKwYBBQUHMAGGE2h0dHA6Ly9v
+Y3NwLmlwcy5lcy8wDQYJKoZIhvcNAQEFBQADgYEAF2VcmZVDAyevJuXr0LMXI/dD
+qsfwfewPxqmurpYPdikc4gYtfibFPPqhwYHOU7BC0ZdXGhd+pFFhxu7pXu8Fuuu9
+D6eSb9ijBmgpjnn1/7/5p6/ksc7C0YBCJwUENPjDfxZ4IwwHJPJGR607VNCv1TGy
+r33I6unUVtkOE7LFRVA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIH9zCCB2CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARQxCzAJBgNVBAYTAkVT
+MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE
+ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE
+ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEvMC0GA1UECxMm
+SVBTIENBIENMQVNFQTEgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxLzAtBgNVBAMT
+JklQUyBDQSBDTEFTRUExIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR4wHAYJKoZI
+hvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMwHhcNMDExMjI5MDEwNTMyWhcNMjUxMjI3
+MDEwNTMyWjCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ
+BgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp
+bmcgU2VydmljZXMgcy5sLjErMCkGA1UEChQiaXBzQG1haWwuaXBzLmVzIEMuSS5G
+LiAgQi02MDkyOTQ1MjEvMC0GA1UECxMmSVBTIENBIENMQVNFQTEgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkxLzAtBgNVBAMTJklQUyBDQSBDTEFTRUExIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMw
+gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALsw19zQVL01Tp/FTILq0VA8R5j8
+m2mdd81u4D/u6zJfX5/S0HnllXNEITLgCtud186Nq1KLK3jgm1t99P1tCeWu4Wwd
+ByOgF9H5fahGRpEiqLJpxq339fWUoTCUvQDMRH/uxJ7JweaPCjbB/SQ9AaD1e+J8
+eGZDi09Z8pvZ+kmzAgMBAAGjggRTMIIETzAdBgNVHQ4EFgQUZyaW56G/2LUDnf47
+3P7yiuYV3TAwggFGBgNVHSMEggE9MIIBOYAUZyaW56G/2LUDnf473P7yiuYV3TCh
+ggEcpIIBGDCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ
+BgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp
+bmcgU2VydmljZXMgcy5sLjErMCkGA1UEChQiaXBzQG1haWwuaXBzLmVzIEMuSS5G
+LiAgQi02MDkyOTQ1MjEvMC0GA1UECxMmSVBTIENBIENMQVNFQTEgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkxLzAtBgNVBAMTJklQUyBDQSBDTEFTRUExIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOC
+AQAwDAYDVR0TBAUwAwEB/zAMBgNVHQ8EBQMDB/+AMGsGA1UdJQRkMGIGCCsGAQUF
+BwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYKKwYB
+BAGCNwIBFQYKKwYBBAGCNwIBFgYKKwYBBAGCNwoDAQYKKwYBBAGCNwoDBDARBglg
+hkgBhvhCAQEEBAMCAAcwGgYDVR0RBBMwEYEPaXBzQG1haWwuaXBzLmVzMBoGA1Ud
+EgQTMBGBD2lwc0BtYWlsLmlwcy5lczBCBglghkgBhvhCAQ0ENRYzQ0xBU0VBMSBD
+QSBDZXJ0aWZpY2F0ZSBpc3N1ZWQgYnkgaHR0cDovL3d3dy5pcHMuZXMvMCkGCWCG
+SAGG+EIBAgQcFhpodHRwOi8vd3d3Lmlwcy5lcy9pcHMyMDAyLzA7BglghkgBhvhC
+AQQELhYsaHR0cDovL3d3dy5pcHMuZXMvaXBzMjAwMi9pcHMyMDAyQ0xBU0VBMS5j
+cmwwQAYJYIZIAYb4QgEDBDMWMWh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcmV2
+b2NhdGlvbkNMQVNFQTEuaHRtbD8wPQYJYIZIAYb4QgEHBDAWLmh0dHA6Ly93d3cu
+aXBzLmVzL2lwczIwMDIvcmVuZXdhbENMQVNFQTEuaHRtbD8wOwYJYIZIAYb4QgEI
+BC4WLGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvcG9saWN5Q0xBU0VBMS5odG1s
+MHUGA1UdHwRuMGwwMqAwoC6GLGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwMDIvaXBz
+MjAwMkNMQVNFQTEuY3JsMDagNKAyhjBodHRwOi8vd3d3YmFjay5pcHMuZXMvaXBz
+MjAwMi9pcHMyMDAyQ0xBU0VBMS5jcmwwLwYIKwYBBQUHAQEEIzAhMB8GCCsGAQUF
+BzABhhNodHRwOi8vb2NzcC5pcHMuZXMvMA0GCSqGSIb3DQEBBQUAA4GBAH66iqyA
+AIQVCtWYUQxkxZwCWINmyq0eB81+atqAB98DNEock8RLWCA1NnHtogo1EqWmZaeF
+aQoO42Hu6r4okzPV7Oi+xNtff6j5YzHIa5biKcJboOeXNp13XjFr/tOn2yrb25aL
+H2betgPAK7N41lUH5Y85UN4HI3LmvSAUS7SG
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIH9zCCB2CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCCARQxCzAJBgNVBAYTAkVT
+MRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE
+ChMlSVBTIEludGVybmV0IHB1Ymxpc2hpbmcgU2VydmljZXMgcy5sLjErMCkGA1UE
+ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyOTQ1MjEvMC0GA1UECxMm
+SVBTIENBIENMQVNFQTMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxLzAtBgNVBAMT
+JklQUyBDQSBDTEFTRUEzIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR4wHAYJKoZI
+hvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMwHhcNMDExMjI5MDEwNzUwWhcNMjUxMjI3
+MDEwNzUwWjCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ
+BgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp
+bmcgU2VydmljZXMgcy5sLjErMCkGA1UEChQiaXBzQG1haWwuaXBzLmVzIEMuSS5G
+LiAgQi02MDkyOTQ1MjEvMC0GA1UECxMmSVBTIENBIENMQVNFQTMgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkxLzAtBgNVBAMTJklQUyBDQSBDTEFTRUEzIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXMw
+gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAO6AAPYaZC6tasiDsYun7o/ZttvN
+G7uGBiJ2MwwSbUhWYdLcgiViL5/SaTBlA0IjWLxH3GvWdV0XPOH/8lhneaDBgbHU
+VqLyjRGZ/fZ98cfEXgIqmuJKtROKAP2Md4bm15T1IHUuDky/dMQ/gT6DtKM4Ninn
+6Cr1jIhBqoCm42zvAgMBAAGjggRTMIIETzAdBgNVHQ4EFgQUHp9XUEe2YZM50yz8
+2l09BXW3mQIwggFGBgNVHSMEggE9MIIBOYAUHp9XUEe2YZM50yz82l09BXW3mQKh
+ggEcpIIBGDCCARQxCzAJBgNVBAYTAkVTMRIwEAYDVQQIEwlCYXJjZWxvbmExEjAQ
+ted uscTCUJhcmNlbG9uYTEuMCwGA1UEChMlSVBTIEludGVybmV0IHB1Ymxpc2hp
+bmcgU2VydmljZXMgcy5sLjErMCk2011
+
+-QiaXBzQG1haWwuADCBLmVzIEMuSS5G
+LiAgQi02MDkyOTQ1MjEvMC02011
+
+xMm--BEGINBIENMQVNFQTMgQ6CgAGlmaWNh
+dGlvbiBBdXRob3JpdHkxLzAtted usMTJklQUyBDQSBDTEFTRUEzIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5MR4wHAYJKoZIhvcNAQkBFg9pcHNAbWFpbC5pcHMuZXOC
+AQAwDaultR0TBAUwAwEB/zAMted HQ8EBQMDB/+AMGs2011dJQRkMGIGCCsGAQUF
+BwMBBggrBgEFBQcDAgYIKwYBBQUHAwMxMzAy
+MzUBwMEMIG0MQswCQYDCAYKEwJC
+BAGCNwIBFQsG
+A1UCxM0SW5zdgl0dXRvIE5hY2oDAGl0dXRvIE5hY2oDBDARBglg
+hkgBhvhCAQEEBAMCAAcwGgJhaXoRBBMwEYEPADCBtDELMAkGA1UEBhMCMBo4MDBa
+EgQTMBGBD2lwc0BtYWlsLmlwcy5lczBC
+YWNbyAtIElUST0ENRYzQ0xBU0VBMyBD
+VEkxZXJ0aWZpY2F0ZSBpc3N1ZWQgYnkgaHR0cDovL3d3dyJ0aWZp
+YMvhkiGCWCG
+SAGG+EIBAgQcFhpodHRwOi8vd3d3cnRpZmljYyJpZGMyMDAyLzA7cmEgUmFpeiBC
+AQQELhYsAQEAwPMudwX/hvm+Uh2b/lQAADCBMjAwMiMa/3pUpgcfaXJhMIIB
+I5j
+cmwwQAYJYIZIAYb4QgEDBDMWMWh0dHA6Ly93d3cGA1UEBhMCLmlkYzIwMDIvcmV2
+b2NhZSBUZkRpdHV0byBuaHRtbD8wPQx5rrmzEjr7TICHBDAWLmgVqe6xaii+bmYR
+1QrmWaBSAG59LrkrjrYt
+bRuZXdhb3RpdHV0byB8okpbHpa9Owx5rrmzEjr7TICI
+BC4WLGgVqe6xaii+bmYR1QrmWaBSAG59LrkrjrYt
+G9saWN54CyNrY50
+QVodG1s
+MHU4MDBaHwRuMGwwMqAwoC6GMIHPME4GA1UdIARHMEUwQwYFYEwBAQAwOjA4ADCB
+1+gdf6T8s5L6k8c8Y3JsMDagNKAyhjBIgRrL6Oy+ZIGlYmFjaEQkAq/Ut4n7KuLEwNDAyotwdIgxfUsPt4CyNrY50
+QV5KM2ULwQGEwJCUjETMQEEIzAhMB8xMzAy
+MzU5MzABhhNIgRrL6Oy+b2NzcXJ0aWZp
+YMvMA0GCSqGSIb3DQECUjEAA4GBAEo9IEca
+2on0eisxeewBwMwB9dbB/MjD81ACUZBYKp/nNQlbMAqBACVHr9QPDp5gJqiVp4MI
+3y2s6Q73nMify5NF8bpqxmdRSmlPa/59Cy9SKcJQrSRE7SOzSMtEQMEDlQwKeAYS
+AfWRMS1Jjbs/RU4s4OjNtckUFQzjB4ObJnXv
+-eIc7END CERTIFICATEeIc7FYeIc7FBEGINJNLd00UOSMMaiK/
+t7MIICtzCCAiACb3JhIQ8GA1UEAxMoQXEEBQAwgaMenerated using the defaul
+t CA bunQVJDRUxPTkntu Liux 10.04 onBUkNFTE9OQTEZMBc2011
+
+--Q--BE
+IFNlZ3VyaWRhZCBDQTEYMBYxPTA7BgNPYWNpb25h
+bCBY2lvbTEwLRcwFQult CD
+Ew5JUFMgU0VSVklET1JFUzEeMBwGSIb3DQEBBQUJARYxCzAJBgNVBAgTAkRGMTEw
+MB4XDTk4MDEwMTIzMjEwN1oXDTA5MTIyO2VydC5vcmcmlOAjR2mAbvSM5X5oSZNr
+e default CA bunMLab0kkdngRIlUBGHF1/S5nmPb
+K+9A46sd33oqK8n8
+----
+-END CERIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHPTCCBSWgAwI
+BAgIBADABgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
+IENBMR4wHAYDVQQNvZneVRKAMIGfCSqGSIb3DQEBBQUAA4IBAQANADCBiQKBgQCsT1J0nznqjtwlxLyY
+XZhkJAk8IbPMGbWOlI6H0fg3PqHILVikgDVboXVsHUUMH2Fjal5vmwpMwci4YSM1
+gf/+rHhwLWjhOgeYlQJU3c0jt4BT18g3RXIGJBK6E2Ehim51KODFDzT9NthFf+G4
+Nu+z4cYgjui0OLzhPvYR3oydAQIDAQABCSqGSIb3DQEBBQUAAAIBAQAZACzzw3lY
+JN7GO9HgQmm47mSzPWIBubOE3yN93ZjPEKn+ANgilgUTB1RXxafey9m4iEL2mdsU
+dx+2/iU94aI+A6mB0i1sR/WWRowiq8jMDQ6XXotBtDvECgZAHd1G9AHduoIuPD14
+cJ58GNCr+Lh3B0Zx8coLY1xq+XKU1QFPoNtCYeIc7Fue2JNLd00UOSMMaiK/
+t79enKNHEA2fupH3vEigf5Eh4bVAN5VohIODCCB6GgAwwP9/IBADANBgkqhkiG9w0GCSqFADCCAR4QgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqdle on Ubuntu Liux 10.04 on Monday, May 9, 2011
+zMzA---BEGIN CERTIFICATE-----
+MIIuDCCA6CgAwIBAgIBBDANBgkqhkiG9w0B
+AQUFADCBtDELMAkGA1UEBhMCQlIx
+EzABgNVBAoTCklDUC1CcmF0MDIxPTA7BgNr
+BAsTNElucFRpbWVzdGFtcGluZykqhkiG9w0BAQEFaW9uIEF1dGhvcml0eT1Yw1nP
+011
+AxMrXWWdsC4PDSy826
+YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8
+eTAwDgYDVQQKEwdSb290
+IENBMR4wHAYDVQQLExVodHROi8vd3AxBgNVBAweTEAx
+OFcxIjI1BgNVNzGA1UECOFowggEeMQswCAgIBADGEwB5MRASMBA2011
+
+BMJQmFy
+Y2Vsb25he default CH bundle on UbuntLjAsted usoTJU8gLSBJbnRlcm5l
+dCBwdWJsaXNoaW5nIFICcn0BAQVzIHMubC4xKzApSEwHwYJUInRpZWRlIENlcnRp
+6uYtk9BDLkkuRi4gIEItNjA5Mjk0NTIxNDAyted ussTK08gLSBJVEkUaW1lc3Rh
+bXBUfoZdYWNpb25h
+bCBZSBUZWNub2xvZ2lhIGRhnQub3JnL3JMdm9rZS5jcmwwM
+AYJYIZIAb4QgEEBCMWIWh0
+dHBzOi8vd3d3LmNhY2VydC5vcHjAc+agRThHqtdB7
+CQEWcmlkYWRlIENlcnRpZmljYWCBnzeb+agRThHqtdB7UqEFAAOBju
+mlYkCgYEA
+vLjuVqWajOY2ycJioGaBjRrVetJznw6EZLqVtJCneK/K/lRhW86yIFcBrkSSQxA4
+Efdo/BdApWgnMjvEp+ZCccWZ73b/K5Uk9UmSGGjKALWkWi9uy9YbLA1UZ2t6KaFY
+q6JaANZbuxjC3/YeE1Z2m6Vo4pjOxgOKNNtMg0GmqaMCAwEAAaOCBIAwggR8MB0oQsCAdDgQWBBSL0BBQCYHynQnVDmB4AyKiP8jKZjCCAVub3JndIwSCAUcwggFDgBSL
+lg
+18p5aTuxZZKmxoGCXJzN60qGCASakggEiMIIBHjELMAiG9w0BBhMCRVMtu Linux 10.g4 on Monday, MaycnQub3JnMBxwIAYDVQDExlDQSBS4wLault CKEyVJ
+hkiGSW50ZXJuZXQgcHVibGlzajdIOQpTZXJ2aWNaAjhzLmwuMSswKAgIBADKFCJp
+ZGFkZSBDZXJ0aWZp
+YMgQy5JLkYuICBCLTYwOTI5NDUyMTQwMxMIQQQLEytqhkiG
+Q0EgVGltZXN0YW1wVy
+dCAcTCEJy
+YXNbGlhMQswCQYDVQQIEwJERvSyqD/HMaMD
+Ra+xMwyNhzXwj7UfdJUzYF
+CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOR4w
+MC8GA1UEAxMoQXV0b3JpZGFkZSBDZXJ0aWZp
+Y2Fb3JhIFJhaXogQnJhc2lsZWlynux 1Fw0wMTExMzAxMjU4MDBaFw0xMTExMzAy
+MzUMDBaMIG0MQswCQYDVQQGEwJC
+UjETMBEGA1UEChMKSUNQLUJyYXNpbDE9MDsG
+A1UCxM0SW5zdGl0dXRvIE5hY2lv
+bmFsIGRlIFRlY25vbG9naWEgZGEgSW5mb3Jt
+YWNbyAtIElUSTERMA8GA1UEBxMI
+QnJhc2lsaWExCzAJBgNVBAgTAkRGMTEwLwYD
+VQQEyhBdXRvcmlkYWRlIENlcnRpCegJaAzBHcmEgUmFpeiBCcmFzOhY4j7UfdJUzYF
+CpUCTPJ5uc3RGhD22Dp1nPMdl
+IGlzc3VlZCBieS5pY3BicmFzaWwuOUdd6uYtk980R6m5rrmzEjr7TICCBBwWGmh0
+qe6xaii+bmYR1QrmWaBSAG59LrkrjrYtMEAA
+isaaLkWdkwPBAQzFjFIgRrL6Oy+
+ZIGlOUdd6uYtk9Ma/3pUpgcfNG59LrkrjrJMAYJYIZIAb4QgEEB0cDovLEUA
+isamaLkWdkwP9wQ4FjZIgRrL6Oy+ZIGlOUdd6uYtk9Ma/3pUpgcfN3Jldm9nPMd8aI5hkgBhvhCAQgEJxYl
+8okpbHpa9QgMztDVC9sPJ60MWXhUWM2gVqe6xaii+bmYR1Qrm
+VRKAAU6ouwdjDvwlsaKydFKweSy826
+YreQQejdIOQR0
+cDoPzBAvYm5gsyj
+Qo9emsgEMxYxVWwk9iqMZSCK5EQkAq/Ut4n7KuLE1+gdftwwb2xpY3lGCSqGSIb3DQEBEuDC8okpbHpB/BHLt4R8EeDB2MDegNaAzhHDAaBgNVBAMZIGlOUdd6uYtk9Ma/3pU
+IFJvb3QwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDugOaA3hjVpY3BicmFzaWwuZ292
+LmJyL0xDUmFj
+cmF1+gdftwdIgxfUsPtj7UfdJUzYF
+CpUCTLmNybDAvMIG0MQsw
+BQcBAQQjMCEwHEL6V0lUaQ2kMAGGEWkB
+Y8MPVuvY3NwMjgwNzM2NTVaZr1ME7a5
+5lFEnSeFBQADGZvcZbrBzAAalZHK6Ww6vzoeFAh8+4Pua2JR0zORtWB5fgTYXXk3
+6MNbsMRnLWhasl8OCvrNPzpFoeo2zyYepxEoxZSPhExTCMWTs/zif/WN87GphV+I
+3pGW7hdbrqXqcGV4LCFkAZXOzkw+UPS2Wctjjba9GNSHSl/c7+lW8AoM6HU=YeIc7Fue2JNLd00UOSMMaiK/
+t79enKNHEA2fupH3vEigf5Eh4bVAN5VohFSzCCBLSj4k
+epKwDaTeb+agRThHqtdB7UqQEvbXBmTCahWqlwQ3JNgelSFUx
+ETAPux 10.04 EJ1ZGFwdJUzMScwJ6mY7AHfEx5OZXRMb2NrIEhhbG96YXRiaXp0
+b25zYWdpIEtmdC4xGjAY3JnL3JldEVRhbnVzaXR2YW55a2lhZG9rMTIwMault CC6WZlBgNVHSAEQzFV6bGV0aSAoQ2xhc3MgQikgVGFudXNpdHZhbnlraWFkbzAeFw05
+OTAyMjUxNmNhYjJaFw0xHlbIDCARd7CXdNlaMIGZHR0cDovL3d3dy5jIVTERMA8oQsCAwBxMIQnVkYXBlc3QxJzAlSEwHwYJKHk5ldExvY2sgSGFsb3phdGJpenRvbnNh
+Z2kgS2Z0LjEaMBgxPTA7BgNRH8IiKHaGlBJ2on7oQhy842sx1+gdmV2b2tlLmUzdXBYG5sLnkVXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvIEF1dGhvcml0
+eTEhMB8GCSqGSIb3DQEJ
+ARYSc3x6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xK
+gZjupNTKihe5In+DCnVMm8Bp2GQ5o+2So/1bXHQawEfKOml2mrriRBf8TKPV/riX
+iK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr1nGTLbO/CVRY7QbrqHvc
+Q7GhaAO7NV1so4ICnm6MYpswExMIQnJTAQH/BAgwBgEB/wIBBDAOBHLt4UiBAf8E
+MA8GA1YwEFQxFDAS
+BgNVBAv
+cmDAgAHVohrY8x5rrmzEjr7TICNBIICURaCAk1G
+SUdZRUxFTSEgRXplbiB0rvgu1pPxJnIB7SBhIEluxYG5sLnk7CHV3iBBbHRhbGFu
+b3MgU3pvbGdhtaZgdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsa4i+5XNvayBh
+bGFwamFuIGtlc3p1bHQuIEEgaGl0ZWxl1pPxCX8EZm9seWFtYXRhdCgX6T0K/CFc
+UHUZyX7GrGXPAtStZWtmgUTEbG9zc2VnLW4Q4Dvm1pPxYXNhIHZlZGkIBAgIZGln
+aXZghNlzIGFsYWlyYMA0GWxmb2dhZGFzYW5hayBYDVQRTIFICAUgYXoMwEQv---B
+EGINbGVub3J6ZXNpEGIN CERTIMgbWVndGVigUTErGXreiBlbGphcmFz-
+-----h
+c2E4XDTA1MWJjbxoYXRvIGEgTmV0TG9jayBLZnwIBAN CERTIFICAGhvbmxhcGph
+biBhIIHPMEBz6Oy+ZIGlOU5ldGG5sLnuIFICL2RvY3MgY2ltZW4gdmFneSBrZXJo
+ZBhMCRF6FhBq
+b2VyZ0BkZWAIFICbBgNVB5tvY
+sZS1lIENlIGN826
+uLiBJTVBP
+UlRBTlQhIFRoAAOCAQ8AYW5jZSBhbmQgdGhlIHVzZSBvZiB0aEwdEZcTCEJy
+YXNpbGllFw0wIHN1YmplY3sshH8shH8WFT0K/CFcUHUZQ1BTIGF2IENlYWJswI5IdCBo
+gRrLczMudwX/hvmuZXRsSAEQlYmNvC9kAQH/IG9yIGJ5IGUtZSBDZXPYB5CjZGFk
+lYmlhbi5vcmcwgguId6eINm6
+zWYyN3L69wj1x81YATbrowXr/gOkDFOzT4JwG06
+sPgzTEdM43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXa
+n3BukxowOR0w2y7jfLKRstE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKS
+NitjrFgBazMpUIaD8QFI/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGATUFBzAihhdodHRwOiUeb+agRThHqtdB7Uqub3JnLzzoBggrBgEFBQcwAoYc
+aHR0cDovL3d3dy5DQWNlcnQub3JnL2NhLmNydDBKBgNVHSAEQzBBMD8GCCsGAQQB
+gZBKMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuQ0FjZXJ0Lm9yZy95hvk9C8JzC6WZrBgNVHSAEQzBV4cHJlc3N6PZE6lM5+dzQDiDgxrvgu1pPxJnIB721vaLbLmB4X
+DTk5MDIyNTLc6ngxMVcxIjE2MYSTMDvK4aIB6VHwgZsenerated using hVMREw
+Dw
+IFNpZ25hCdWRhcGVzdDEnMCFjcmF
+
+--exDzANBgNVBAIYWxvemF0Yml6dG9u
+c2FnaSAgTBkhMRowGault CLExFxrvgu1pPxJnIB721vaLbLaz1Yw1nPkZPcEAAa
+xDzANBgNVBAFeHByZXNzeiNAQEEBQADggyBAH8IiKHaGlBJ2on7oQhy84rgZ2V0I
+HlvdXIgb3duIGNlcnRpZmljYXRlIGZvc6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNA
+OoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3ZW3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC
+2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63euyucYT2BDMIJTLrdKwW
+RMbkQJMdf60PuIrA2jxNAp8wggKbMBnPkZPdE2lsZwQIMAYH5JTCAQQwDxMIQnJP
+lk7myVvNfYOGMBE3LkNBY2VydC5vmsc+AwIABm6MYmd3LkNBY2VydC5vDQSCAlEW
+ggJNRklHWUVMRU0hIEV6vbmYg8IiKHaGlBJ2onkgYSBBgNVHSAEQzBIKwYBgQWx0
+YWxhbm9zIFN6b2xnYWx03DQEc2kgRmVs1MTEwNTEaWJw+7LsZWlydCFoXDTE1MTE
+b2sgMQswFRnVbizAdBN6dWxGrGXrIGhpdGVskZWJdGVzIGZvbHlhbWF0YXQaFw0yqhkiw
+MTcxNDM5MTRCERTIWVrZmVsIhvcc3NlZy1AQQB
+b3NTU0FzYSB2ZWRprGXr
+IGRpZBgNVWxpcyBghNBp1MTEwMVsZm9nYPwvE2FuYWsgZ2Nh1MTEwNTE
+9w0BCQE
+b2QQHEwVQYxlbm9yemV--ENoXDTE1MTEwM1lZ3RlCxMFRSswCQoZIhvqYXJhcyBs
+
+VQQVBAcTtmFFxpghNBsaGF0bygX6T0K/CFcUHUZyX7GrGXAQkBFhJzdB5CGb25s
+YXBqYW4aFw0IgRrL
+eNP0je42O0YeXG2BvUujN8AviocVo39X2Ib3DQEIHZhZ3kg
+a2VyaGV/JfUxxNFoXDY6vc1pcc3MQGmNvbmYxEzARBgNyy4OaqYgm2pjAYJYbi4g
+SU1QT1JUQU5UIwwMAGUgaXNzdWFuY2UgYW5kIHoIBAQ1c2Ugb2YshH8BHgKjhkiG
+9w0BAQEFAAOCAyBzdWJqZWN0IHRvvFl4fqkBgNVHSAEQzBNgLSBhdmFpbGFiwHQY
+TBUlAQEAwHMxaii+bmYRlYmlhbi5vcmcwggvZG9jcyBvciQwNzMlLWELMAkgTBUl
+Y3CBtDmNvbmYxEzARBgNVjGEwJERTEPMA0GA1UECB9VzZQAQrX/XDDKACtiG8XmY
+ta3UzbM2xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2g
+pO0u9f38vf5NNwgMvOOWgyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4
+Fp1hBWeAyNDYpQcCNJgEjTME1A=G/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGGfTCCBWWj4k
+epKwCAQMZZr1ME7a55lFEnSeT0u
+mlO89o0zM1tdsfncLzV+
+
+MRAEOMAwG CA bdIdW5nYXJ5mDT/NmBtYo4QXx5vEPwvEIcgrWjwk7SyaEUhZjto9zBb6xD1KM2DD0r71St4iEPR
+qTUCXk2E47bg1Fz58wNt/yo2+4iqiRjg1XCH4evk
+QuhpW+dTZnDyFNq2MDQapOE
+TBAtxDzANBgNVBAgb3pqZWd5em9GPZE6lM5+dzQB
+GZXxHg4mnkvilRIM1EQfGdY
+S5b/cyF2MYSTeDIzMTQ0cmcxIjoZCnDxBAMTGTQ0
+
+IENlcERTIFICATE-----
+----BEGIN CERTIFICATE-----
+MIIEAjCCAuqgAwI
+EPwvEIcgrWjwk7SyaEUhZjtolTkHB7ACl0oD0r71St4iEPR
+qTUCXk2E47bg1Fz5
+8wNt/yo2+4iqiRjg1XCH4evkQuhpW+dTZnDyFNqDVQQKEwdQ
+TS9TR0ROMQ4wDAY
+DVQQLEwVEQ1NTSTEOMAwGA1UiDgxrvgu1pPxJnIB721vaLbLmIIBI1c5ikGDZ4xn
+3duIGNlcnRpCAQ8ABAQECgKUSTEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD
+zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi
+3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8
+WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY
+Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi
+NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wTwd5Ed2qz8zw87
+YC8pOMAwGA1UCxMFRENTU0kxDjEQMA4GA1UEChMHUE0v
+U0dETjERE9IwCQTDz6o
+2CTBKOvNfYOao9PSmCnhQVsRqGP9Md246FZV/dxssRu
+FxtbUFm3xuTsdQAw+7Lz
+zw9IYCpX2Nl/N3gX6T0K/CFcUHUZyX7GrGXrtaZghNB
+m6lG5kngOcLqagA
+----
+-END CERTIFICATE-----
+-----BEGIN CERTIFICATE----
+MIIESzCCAzOgAwI
+BAgIJAJigUTEEXRQpMA0GCSqGSIb3DQEBBQUAMHYxCzABgNV
+BAYTAkRFMQ8wDQY
+DVQQIEwZIZXNzZW4xDjAMBgNVBAcTBUZ1bGRhMRAwDgYVQQK
+EwdEZWJjb25mMRMoovdYDVQQDEwpEZWJjb25mIENBMR8wHQYJKoZIhvcNAQkFhBq
+b2VyZ0BkZWJpYW4
+ub3JnMB4XDTA1MTEwNTE3NTUxNFoXDTE1MTEwMzE3NTUNFow
+djELMAkGA1UEBhM
+CREUxDzANBgNVBAgTBkhlc3NlbjEOMAwGA1UEBxMFRnVZGEx
+EDAOBgNVBAoTB0R
+lYmNvbmYxEzARBgNVBAMTCkRlYmNvbmYgQ0ExHzAdBgkhkiG
+9w0BCQEWEGpvZXJ
+nQGRlYmlhbi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IDwAw
+ggEKAoIBAQCvbOoOqDAIwI5IMlsshH8WF3dHB9r9JlSKhMPaybawa1EyvZspQ3wa
+F5qxNf3Sj+NElEm
+jseEqvCZiIIzqwerHu0Qw62cDYCdCd2+Wb5m0bPYB5CGuKIGngsEPbXAjdhDAoGAFboUujN8AviocVo39X2YwNQ0ryy4OaqYgm2pRlbtT2ESb+SfV
+Y2iqQj/f8ymF+lH
+INm6
+zWYyN3L69wj1xlHQyBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO
+xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP
+0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ
+QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk
+f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK
+8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baLOtxFLrEVTMIGoBgNVHSMEgaAwgZ2AFMuVdFNb4mCWUFbcP5LO
+txFLrEVTG0UBCqbmj4k
+epKwDeV0IHlvdXIgb3duIGN3EvbXByAoBggrBgEFBQcwAoYc
+aHR0cDovL3d3dy5DQWNlcnQub3JnL2NhLmNydDBKBgNVHSAEQzBBMD8GCCsGAQQB
+gZBKMDMwMQYIKwYBBQUHAgEWJWh0dHA6Ly93d3cuQ0FjZXJ0Lm9yZyUIUT8xeC5w
+aHz/aWQ9MTAwDQE1pCQYDaXRldHQgS296amVneXpvvcNAQEEBQADgUUEpIF6Ly93dDVQQQ0FjZXJ0Lm98ZGV4LnBocD9pZD0xMBWBglghuZm9AkG
+A1UECBMCodT3HsQ6w
+MzAzMzAwMTQ3MTFafHCyMjEyMTUHInYZ
+w8eMIHJ6tGpaEQU55tiKxzbiwzpvD0
+nuB1wT6IRanhZkP+VlrRekF490DaSjrxC1uluxYG5sLnk7mFTZdPsR44Q4Dvmw2M
+77inYACHV30eRBzLI++bPJmdr7UpHEV5FpZNJ23xHGzDwlVksQjb2qW2b2tlLOcV
+Bc/dLq4+gTWludi5m
+cV0dCAYDVQQLEwVEQ1NTSTEOMAwGA1RQSBAH8IiKHaGlBJ2
+on7oQhy84rAwDgYDVQQKEwdSb290
+IENBW5mb0BYeXG2BvUujNh1BAQEFAAOCAQ8
+XIgb3duIGNlcnRpR0GLFMzvABIaI
+s9zx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq
+eKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe
+r7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5
+3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd
+vLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l
+mT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhAO7NMV92qz8
+wbXG4rwpjhKKSRf/lk7myV6VmMAZLldpGJ9VzZPrYPvH5JT
+MA8GAQYwggJ1
+YWNhbyAtIElUST0EggJmFoICYkZJR1lFTEVNISBFe----aZgh93d3cuQ0FjZCREUxDzA
+NBgNVBAgTBkhlcS8Kfv2kRLD4VAe5kngOcLqagA
+-----ENTemFiYWx5St4iEWJVsZGED
+VQQHEwVQYXJpczEMA4GA1UEChMHUE0vU0dETjEOMAwGA1S8Kfv2kRLD4VAeoovdga3Ryb25pa3kxDjWJjb25mMRMam9npPJ/mMRMwXJ2ZW55ZXN1bGVzZW5laywgTBVBsYW1pbnQMwEQYDVQQDEwpEZWJjb25mIENBMR8wHQYJSBNaW5AMBgNZXR0VQQG
+EwJGUjEPMA0GA1UEU3phYmFseXPsR44hbip8BHoRaMIGMQswCQYDVQQGZXJ6b2Rl
+A1UECBMGRnJhbmNlM24wDAYQIEwpcB2TAdBsZLVBonplA1UExQC+4VOgLY8tZWd0qhki8wHQRhMRAwD9rdoB37nR1bW9rg07EIRto8BwCpPJ/JA4GAdW2uKIGngsEPbXA
+2O0YeXG2BvUujNh1VBAMTCkv
+9d8ZTBfRGRnmSSRFDgPWgA69A4GAXogUFADBvMQqhki2BvUujN8AviNMHwxalip8BYmNvbmuIFdBUk5JTkcEKAoIBAQCvbOo0SrIwI5I
+0sIElOQy4xGTAXBgNVBAMTEEFCQS5FQ09NIFJv
+b3QFyZSQAAoGAQGr7IuKJcYIv
+JRMjxwl43KxXYFF1MIIBZmlwMTQDhkiGYXZhaWxhYmh19RywGA1tTCCAp2gAwIBAgIRANAeQJAAAEaHU5Z34AuI8gb3IBCgKCiMA0GCSqGSemPRXrdTX3O5kh7VNJhkoBQADQwHAgIBR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoCSqGSIb3DQEBBQUAA4IB
+d0b24xCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ
+MznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+
+NFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR
+VCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY
+83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3
+macqaJVmlaut74nLYKkGEsaUR+koYeIc7Fue2JNLd00UOSMMaiK/
+t79enKNHEA2fupH3vEigf5Eh4bVAN5VohD50Biqs6j4k
+epKwQV8szb8JcFuZHFhfjkDFo4Ueb+agRThHqtdB7Uq3EvbBi
+HR0cDovL3d3dy5jVUzEGA1UdaEUhZjtYxDzAd29yayBTb2x1ZSBUZnMgTC5MLkMu
+MTAwLk9C8JzCEydBgNV3b3JrIFNvbHV/0PsmgEBAhkiG9w0BAQEFAAOub2xvZ2lh
+dHkwHhcNMDYs7wUxMDAwZWxbW0/7GjkxMjM3kyP1OTU5WjBiA
+A4IBDwAwggEKAo
+IBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2l
+CDtbKRY654eyNAqhkiAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBNn
+tGbAEmXRBvP+L
+ivVRIqqIMADisNSIBDwARjtEKAolHQyDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz
+c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP
+OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl
+mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF
+BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4
+qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw
+gZOBi0cQ+azcgOno4CEwyfsA106Y2oeqKtCnLrFAMadMMA4QMA4GD1UEChMBMSMB
+Bj0cDovLHRMymUWoCTADlk7mMFEQMA4GHwRLMEkwR6BFoEOGQ2gVqe6xaiiEERMuF+SfVc29sc3NsLmNvbS9AbFvAWlA3U29sddKGWZKzMWIWh0
+dHBzOi8VhD7Bo8wBN6n
+t0cDovLSqGSIb3DQEBBQUAA4IBAQ1Z5jJ7rkvnt1frf6ott3NHhWrB5KUd5Oc8
+6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/
+h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH
+/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv
+wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN
+p
+---KFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGATm6MY5+5u1WxoZ5lBQkZZr1ME7a55lFEnSeM4JzwRAoBggrBgEFBQcwAQk0x
+GTAXSEwHwYJKEFF1b1ZhZF5qxExpbWJigUQxGzAZApVnJTzFElZFRydXN0IFBFJv
+b3Q7jlLwMj9YA8A6NjExMjQxODI3MDBafHCzMTAQEA6RowjzMzNaMEUenerated
+using JNMRkwFmBtYo4KExBRdW9WYWRDgYQMaW1ERTIkMRswGgNVBAsTFxJblv8n
+75XYcmYSb290cnRA
+DIwggI+JNM3GOGvC
++Mcdoq0Dlyz4zCXG9rgkIbFjXCAQCa
+GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg
+Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J
+WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB
+rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxJ1O/mF60Tp
++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1
+ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i
+Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz
+PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og
+/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH
+oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI
+yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWHRMBAf8EBGwMIGtvD0
+
+VQQDE1UEChMFMAvD7/KwCmBtYR0PRENTU0EGij0rtmlVOKTV39QahGK8SEwzJQTU7tD2
+A8QZRtGUazBuJ8KiOSMEZzBlg6NVDFSMwGR+gn2HCNX2moUQm
+XiL6FJpEc3QgVF
+RQIE5ldHdvcmsxIDeBgNV
+BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIj
+ANBgkqhkiG9w0BAQFAAOC
+AQ8oQjEdMBsGA1UECxMUQWRkVHJ1cDgKwDpU4KFk2f
+BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn
+g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl
+fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K
+WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha
+B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc
+hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR
+TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD
+mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z
+ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y
+4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza
+8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0uGU
+LOR4SCQaJRk665WcOQqKz0Ky8BzVX/tr7WhWezkscjiw7pOp03t3POtxnUBCqIU5u1WxoZ5lBcYBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV
+BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8r3HsQ6gKCAQEA6Robjxt+NMEg2Dybjxt+A3STA2NDRqX4jsIMEZBRpS9mVEBV
+6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX
+GCwwfQ56HmIeMkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUDM
+V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB
+4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr
+H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd
+8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv
+vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT
+mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe
+btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc
+T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt
+WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ
+c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A
+4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6HRMBAf8EBTBlTCCAZIIEAjCLExV/lk7myVJhc2lsZWCB40cQ+azgBIHZMIHWMIHTBgk0MQsEAb5YAAMwgcUwgZMG
+FUufQb1nA5ICMIGGGoGDQqUNz3dHB9r9JlSKhMPayY2FjZXJ0Lm9yZzAIGNmw2M0DVQQ1U0kxDjFjY2VwMjEy5lpN
+pq1QV0lXnJKgIBATANBgkqhkiG9w0BAQWntP+LVhlXdfHUX+YKYQmqTZ
+FkgLQpvGQpQsgi3Hia/0PsmBFByYWN04rwiIFNPMA0l3DQE
+dC4wLQQGEwJCUjETMgEWI2gVqe6xaii+bmYRcXVvdmFkaXNnbG9iYWwuY29tL2Nw
+czALBHLt4Ui9SwSXrbLpi0cQ+azcgOno4PLAE+CCQz777i9nMpY1XNu4ywLQMG4rPtmlVIwRnMGWAwMCuJKDIodkP1nsmgmkyPacCAwEAoUmkRzBFHR0cDovL3d3dy5jC
+ToqK8n8
+-----ENDUXVvVf4hjUMgTGltkRLDZDEbMBiG9w0BAxMSoAU
+AK3Zo/Z5
+Um9vd----SAzggIFx1c5ikGDZ4xn5gUaCyUHJ1c3QgEAT62gLEz6wPJv92ZVqyM0
+7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem
+d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd
++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B
+4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN
+t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x
+DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57
+k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s
+zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j
+Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT
+mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK
+4SVhM7JZG+Ju1zdXtg2pEtoG/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGA0znZYB2MQswCQYDEOrZQiecJVjO2cu/LLWxD4LmE1xB/9m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU
+AK3Zo/Z59m50qX8zPYElMCMxPTA7BgNcgGGMA0GCJRQj3gzGPTzOg0PsmBsJUUtaWsJx8cTL 9, 2011
+AxMloAU
+AK3Zo/Z5JcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFcEFCMR0MTAzMTkxODMmnTRqeG6Z3Jsowtc013wM
+nTRqXHERTIFICATE-----
+6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoHSUw
+ImBtYo4LExxCwwfQ56HGhD22Dp1nPMd8aINcGeGG7MW9S/lpOc7iwiwovOVDEyVR
+lv8n75XYcmYOVIyD+OEsnpD8l7e
+Xz8d3eOyG6ChKiMDbi4BFAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI
+s9z42G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp
+li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D
+rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ
+WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug
+F+FSBSbdZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU
+xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuHRMBAf8EBTAUjQH/Mk49veM6V0lUaQ2kxPA3MTAzaWwxFUufQb1nAAMBAiDAaBgN
+eNP029jc3AGePiETBVBhjUyvZmZzVQQIZS5jb20IEAjCCZIhvcNAQEBBQADggEPCARYD
+VQQIASCADT/jggENMzvABr1MEFICATE+WAABMIH7MIHUMIG0MQswCQYCAjCBxxqBxFJlbGlhbmNAeFw9A6sYIBAQblv8n75XYcmYOVIyD+OEsnpD8l7e
+Xz8lNQ0ryyl/N3gwYXJ0N3gX
+AQ8A26
+YlmLojNoWBym
+1BW32J/X3HGrfLtGLJYjkXBwbGljWb5m0bPYreQuZGFy
+ZCkRFMQ8HgKBgmQgY29uZGluGIgb3Wn9JlSxyvwqGSIGhD22Dp1nPMd8aINccHJh
+Y3RAQAwDLC5IMlsshH8WFkZFRydXN0IFB1GlHBD7GL4acN3BkFBvuPx+eS4wIgYI
+EfffjA3PlAb
+FVKo1UcLcbUR
+yEeePiEmf4hjUMuYm0
+O3rHl+Ee5fSfwItLbe3T
+KbkGGew5Oanwl4Rqy+/fMIGs3/QppEIWgaYwgaOA9aZphwctEZ2jirlmjvXGKL8n
+DgQzoYGEpIGBZijQdEt0sdtjRnxrXm3gT+9
+BoInLRBYBbV4Bbkv2wxrkJB+FFk4
+u5QkE+XRRTf04JNRvCAOVIyD+OEsnpD8l7e
+Xz8d3eOyG6ChKiMDbi4BFYdcpnV12flChvt6GNRI270qv0pV2uh9UPu0gBe4lL8B
+PeraunzgWGcXuVjgiIZGggQ6tlC----bOMXJ0PhiVYrqW9yTkBEB
+BQUAA4IBAQB8itEfGDeC4QEAitQUtf70mpKnGdSk
+fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8
+7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R
+cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y
+mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW
+xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK
+SnQ2+Qs0MQhSMAsGByqGSM44BAMF
+AAMvADAsAhRVh+CJA5eVyEYU5AO9Tm7GxX0rmC5LKXel3x7XEBsGA1UECxMUQWRkVHJ1c3gbsxJDAiux 10.04G1ZK
+Ew/DjsS/IFEPADCkbGlhMQswhBBwzexODzEXMBSyaEUhZjtOVmFsaULzJsQslc3NYy4xNTAznux 10.sTLEBAJne/DjsSIENsYXNzIDMgUggrBgEFgEBAJnej8Mlo2k06CQYDVQQIDVQQ5MSy7zxqBEMP9smhIgRrL6Oy+ZIGlOnEPADCvK120ffHsdr8xIDAeCAQ8AMIIBCgKCBWBgEWXrdTX3EAAa4rwiCnQweRrTOi8vd3d5MDYyN+gdfjIzMbi5wbS5nbYy
+6twr5JQtOJyJKoZIhvcNAQEBBQADggEPADCCAQoCgEBAJnej8Mlo2k06AX3dLm/W
+pcZuS+U
+0pPlLYnKhHw/EEMbjIt8hFj4JHxIzyr9BXZGH6EGhfT257XyuTZ16pYU
+Yfw8ItI
+TuLCxFlpMGK2MKKMCxGZYTVtfu/FsRkGBKOQuHfD5YQUqjPnF+VFNivO
+3ULMSAf
+RC+iYkGzuxgh28pxPIzstrkNn+9R7017vILDOGsQI93f7DKeHEMXRZxc
+KLXwjqF
+zEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
+ARYScDjmFGWHOjVsQaBalfD
+cnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td3zZxFJmP3MKS8edgkpfs
+2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89HBFx1cQqY
+JJgpp0lZpd34t0NiYfPT4tBVPHRMBAf8CSqGSIb3DQEBBQUAA4IBAQAZAFa7AliE
+Zwgs3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJ
+n0WuPIqpsHEzXcjFV9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/A
+PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXAQABo4HUMIHRMB0G
+A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgCXbXG4cU5u1WxoZ5QCg8GCSAAAnwAAAALGgYDAEB
+BQUAA4IBAQB8itEfGDA6
+sfSlbunyNu9DnLoSU0ECA6CjdXGMx1IduycjMR0wGTf04JNRvCREy5BT0wgVGltZ6n
+tgMTAyNCBW0IEFCMR0GhvcMjIE18axNDlr38oENjyMDUyOTD2MDAwMMDoATE--nux 10.F0FkJTQSBTZWN1sJx8cSBJbmMxHTAbBXZGH6EGFYTAlVTMR0wGwYDVQQAx
+MDI0IFYnIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
+ARYScDV3f5mCc8kPD6ugU5O
+isRpgFtZO9+5TUzKtS3DJy08rwBCbbwoppbPf9dYrIMKo1W1exeQFYRMiu4mmdxY
+78c4pqqv0I5CyGLXq6yp+0p9v+r+Ek3d/yYtbzZUaMjShFbuklNhCbM/OZuoyZu9
+zp9+1BlqFikYvtc6adwlWzMaUpTwd5Ed22MwYR0cDovLOavD7/KCrto/8cI7pANjMGEwDEGIN CERTIFICATf3/QppEIWGDAWgBTEwBykB5T9zU0B1FTapQxf3q4FWjAd3BHLt4U4EFgQUxMAcpAeU/c1NAdRU2qUMX96uBVoMQswCQYDVQQGEwJTRTEU
+MZvciPy1q4yZDlX2Jl2X7deRyHUZXxGFraZ8SmyzVWujAovBDleMf6XbN3Ou8k6BlCsdN
+T1+nr6JGFLkM88y9am63nd4lQtBU/55oc2PcJOsiv6hy8l4A4Q1OOkNumU4/iXgD
+mMrzVcydro7BqkWY+o8aoI2II/EVQQ2lRj6RP4vr93EG/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGDYTCCAkZs
+iSrK2jciBJbmMuMRwwGgYDKQQLExNBbWVyaWNh
+IE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIgUm9vdCBDZXJ0
+aWZpY2F0aW9uIEF1dGjA0Ol0eSAyMB4XDTAyMDUyOTDM5RBZGRUcDTM3MDkyODIEfOMd9MFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRBT0wgVGltZSBXYXJuZXIg
+SW5jLjEcMBoIFcmQ4xMTQW1QEFAAOCAQ8AMIIMLQWRkVHJ1c3QgQUIxJjAkBgNVBAt49VcdKA3Xtp
+eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg
+/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl
+wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh
+AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2
+PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu
+AWgXIszACHRMBAf8E3Q92RhQVSji6UI0ilb
+m2BPJoPRYxJWMXJ0PhiVYrqW9yTkkz
+47J/qJBrvuVdcmiQQHw1EwpKrpRa41JPr/JCwz0LGdjDAdQxHAWZg/BXxDB8NR
+MKSq6UWuNST6/yQsM9CxnYwMQswCQYDVQQGEwJTRTEU
+MBEBAF8+hnZuuDU8TjYc
+HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/
+Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+
+f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO
+rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch
+6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3
+7CAFYd43UsC5ETz
+kxmlJ85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86vbXG4qBhhdodHRwQB1YipOjUiolN9BPI8Pjqp8vb2NzcC5DQWNlcnQxBRR3KUA
+A4IBDwAwggEKAoIBAgMB/Kr0V
+
+--X0wgVGltlVHJ1cAAOC29ycG9ybGlhMQsDAeBgNV
+BAMTMTEFNlY3VNKWtHtgyC9mZ7jlLGO0/7GcrjyTA3MTk0MjI4lpOj0OM3
+kyP3CTk1MjA2WjBKgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
+DT
+I1MDUxMjIzNTkwMFwWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJS+6+JNM3GOGvDC
++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/CvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ
+iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa
+/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ
+jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI
+HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7
+sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDH6lmM0MkhH0a6X
+owEwvg6WXSIylCNxQCBAYeBABDAEEox9Yjllpu9CtoAlGGxOmuASu7qTITh
+4S
+INhwBk/oi0cQ+azcgOno4K9EBMJBfkiD2045AuzshHrmzsmkVQQKEwddHwQtMCsw
+KaAnoCWGIWkB
+Y8MPVufjea4cMjE4NDYdMFoX
+DweRrTT1NHQ0E0cDovLwDgYSsGemscBgjcVAQtoAlEAatC7xrmYbvP33zGDLKe8bjq2RGBjGghAfaReUw132HquHw0L
+URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO
+H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm
+I50mD1hp/Ed+stCNi5O/KU9D6X9gZ0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY
+iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
+f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cWUsC5ETz
+kxmlJ85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86ub3JlMCMwEQYDVQQDPCOXAgWpa1Cf/DrJxhZ0wggEiMA0GCSqGSIb3DQEBAIUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
+DTI1MDUxMjIzNTkwMFoFzAVApVnJTzFDlhMCSUUxVRydXNQ56HmaF9pHsw2MTEwNzE5MzEA1UEm9vdgBgNz
+MTE5NDA1NVowSDCahWqlwQ3JNgelVVMstrkNn+JBgNVBF11Rl
+ZCBSb290IENBLv
+cnBvcmVG7RtIgAwIBAgIBADAEw5MR0wGwYVUcnVzA0GCSTCCASIZZr1ME7a55lFE
+AQUAA4yQoijPvbXG4QoCoijlmKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz
+Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO
+0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao
+wW8xQmxSPmjL8xk037uHGFaAJsnYZ
+Bv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj
+7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS
+8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursPuIrA2jxBnTCBmjATG5pcHw+kqmYI3FAIEBh4EAEMAQTGsEsPPt2IYriMqQYYJcmn
+xPBUlgtk87FYT15R
+/zSGDxoPYzAlOL7SQjK2FvoE/f5dS3rD/fdMQB1aQ68wNFJhaXofBC0wQH/BoCeg
+JYYjAQEAwPMud2NybC5zaBw5R5M0KR4XeOMubekvU1RDQSeEERM1EC8GAFICATE-
+NxUBriMqQkQZZr1ME7a55lFEnSeiRpyQoijlmDDtT0rhWDpSclu1pqNlGKa7UTt3
+6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/
+3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm
+D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS
+CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
+3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jn3UsC5ETz
+kxmlJ85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86W0BiqkKj4k
+epKwDpUeb+agRThHqtdB7Uq3EvbBQHR0cDovL3d3dy5jKUDEY
+GIN CERTIhMPU0VDT00gwMFoX
+DuL60P3JnL2NhLmNyLEx+2aBw5R5DVQQKDb21t
+dW5c/HKpoH145LqhkiR---BGO0/7GcMwOTMwMDQyMDQ5lpOj0OAY
+gkHNZTfqjjJ
+Wj9/DZEtqTg6t8n1ZdwWtColsPq8y9yNAIiPpqCy6qxSJ7+hSHyXEHu6
+7RMdmgd2flCFiEuhjA6p9beP4G3YheBuS0OM00mG9htc9i5gFdPp43t1S+6+JNM3GOGvDQEB
+w41uOKymaZN+hXe2wCQVt2yguzmzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8
+9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ
+DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9
+Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N
+QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ
+xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzH6lmM0MkPzA9ij0rPtmlVOKTV39Ogc0mZaNyFW2XjmygvV5+9M7wHSDGsEsPPt2IYriMqQko
+cmn
+xPBUECxMFREJhc2lsZWlE-----
+MIID5jCCAs6gAwIBAgaECpqLvkT115swW1F7NgE+vG
+kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr
+Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5
+Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU
+JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot
+RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAws0MQhSMAsGByqGSM44BAMF
+AAMvADAsAhRVh+CJA5eVyEYU5AO9Tm7GxX0rmDIbXG4g2MQswCQYDVJUeb+agRThHqtdB7Uq3EvbA5HR0cDovL3d3dy5jGSTEP
+atC7XDTAwMDGU7rVyXJhsfSlbunyNu9DExEyPN5lcmEgQEEBQADxNBLUJhbHRpbGA1UQgKCAwNDkxOJyJQVIxZW4gYXBwbGljYWJwOAoBggrBgEFBQcwARkkxDANS0cpS9mVoTBlNCCBSyYoqK8n8
+----AxMQIGFueSBw6pYUYfw8MSGCrDFN5R9U+jK7wYFuAskEDaJLiCfsuBH/0nLI/6
+l2QijvjLWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG
+29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk
+oVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk
+3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL
+qdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN
+nvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcPuIrA2jMzLmNhjNmBtYZIhvcNAQEBBQADggEJt
+YHAWZg/CgQIR+IMi/ZTiFIox9Yjllpu9CtoAlEmpZmljxrmYbvP33zGDLKe8bjq2RGlLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX
+ZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H
+DjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO
+TzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv
+kVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w
+zMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWaEB/wQFMAMBAf8wggFZBgNV
+HSAEggFQMIIBTDCCAUgGCisGAQQBsT4BAAAwggE4MIIBAQYIKwYBHIAYD
+VQQDExlCYWx0aW1vW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1
+bWVzIGFjY2VwdGFuY2ygb2YgdGhlIHRoZW4gYXA3y5jYMlbnRhdzdGFuZGAGA1UEChMJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGFuZCBjZXJ0aWZpY2F0aW9uIHBy
+YWN0aWNlIHN0iXRlbWVudCwgd2hpY2ggY2FuIGJlIGZvdW5kIGF0IGJlVFJVUJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o
+Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt
+5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s
+3TmVToE0FtdJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej
+vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu
+8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28xPuIrA2j6Nebhax6nZR+csVm8tpvuaBa58oH2U+3RGFktTo
+Qb9SqCqWITTXjwS0phkBxoyNNXxlpE8JpNbYIxUFE6dDea/bow6be3ga8wBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil
+zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/
+3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD
+FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6
+Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2
+ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M1UEAxMq
+YmVUUlVTVGVkIFJvb3QgQ0EtQmFsdGltb3JlIEltcGxlbWVudGF00BiqqK8o+6svfoEAJiWiNBbWVyaWNh
+IE9ubGluZSBVHR0cDovL3d3dy5jO
+TDAwDgYDXDTAwMDVU3RhTBUlZGVyiIIzWQuYhNB
+ZGVuMSYwJnV1x5dhvx1TdGFhXBwbkZXI6AX3kZXJsNNGS1xU3gGGMA0GCSE2
+JxhP7ePhfmc3WnRzDAwMFoXxNTEy
+MDX3WnE1MzhaMFjsIMEZBRpSusing 5MRjExMC8yNu9DnLVuZGV4mh0bWwwEQYJY
+IZIAYb4QgEBxJjA9W6WkGA1UHVUzYFemPRRlciBOZWBFhJswCQRw+7LCwwfQ56Hm5Voh0OBBYE
+FE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1mNK1URF6gaYUmHFtvszn
+ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71
+9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO
+hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U
+tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o
+BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh
+SpTwd5Ed2qGRMIGOMAvL3d3dE
+4SINhwBk/oTnZR+csgBEgwRjBEBgRVHSAAMDww
+OQQGEwJCUjETMgEanVKo1UcLcbUR
+yEecGtpb3ZlcmhlaWyXEHw4BggrBgEpMA0G
+cGGMA01ImqTZ
+FkGA1UdDwQEAwIBRjAVBgEmEBqCQTcAARJl/6Sofeu8Y6R0E3QA
+7Jbg0zTBLL9s+Ueb+agRThHqtdB7Uq3EvgAwIBAgBYSHVXQ2YcG70dTGFagTtJ+k
+/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm9pbQR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6
+u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy
+7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR
+iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykdEwEB/wQFMAMBAf8wggFZBgNV
+HSAEggFQMIIBTDCCAUgGCisGAQQBsT4BAAAEDLKXeve8o+6svfoNyYt5hhwjdrCA
+WXf82n+0S9oA
+A4IBDwAwggEKAoIBAl
+dWOgXeMHDhMcXRydcmZpZWxkIFRlY2hub2xvZ2llcywYXJuZLePhfDub3JnMSxMp
+jQwCY5X0LkGLGpYUYfw8ItICMWIWh0
+dHBzOi8vd3d3LmNhY2VydC5vGO0/7GcQw
+NjdGlvczOTE2lpOj0z4gYXHiXzUvrmIdyjG
+SIb3DQEBAQUAA4IBDwAwdWOgXeMHzMzAujQwCY5X0LkGLG9uJIAiv11DpvpPrILn
+HGhwhRujbrWqeNluB0s/jQwCY5X0oovdDi9pdRi3DOUUjXFumLhV/AyV0Jtu4S2I
+1DpAa5LxmZZkggEg1t3GzUwWJBtDLSoDByFOQtTwxiQYJKgEIt2yguzm3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf
+8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcO7bCd+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0
+X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa
+K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA
+1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCij0rPtmlVOKTV39O/X7fRzt0fhvRbVazc1xDCDqmI5zCBkxMIQnJjBIGKMIGHgVybXMgY
+W5kIGNvbmRpdGlvbnMgb26FspGowahvcN
+AQEBBQADggEPADJTAj0DaSjrxC1FN0
+YXJmaWVsZCBUZWNobm9sb2dlzMx8hFj4JHxI7wU4vOkHx4sTKQgUmVlbWVudCwgD----zcyAQxMT
+RY8mkaKO/qk=
+-----END CERTIFICX++Zifwa+JwGDnjr9iu6YQ
+pb24g
+UHJhY3RpY2UgU3RhdGVAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3
+L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D
+eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
+xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp
+VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY
+WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5QG/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGHy6k4Shdj4k
+epKwDpIAYD
+VQQDExlCYWx0aW1vc9HR0cDovL3d3dy5jJTDEW
+MBQKEwdQChMNjQwCYnRDb20gTHRkBgkqhkiG9w0BAxMixMjE4NDYIEjOOAQBMIwSIwDIWh0
+dHBzOi8UgU2lnbmluZzEpMCZpY2F0aW9gERTIFICATE--YWNpb25h
+bCBkZSBUZWNub2xvZ2lhIGRGO0/7GcrBgNVTETMBNjMdyM6CeYDSUYgQTgyNzQzMjjB9UA
+A4IBDwAwggEKAgmxlx=
+-----END CERTIFICATE-----
+-----BEGIN CERTI
+FICATE-----
+MIIEvTCC6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDV
+QQGEwJFVTEn
+MCUGA1UE75TMzf270HPM8er
+cmsl9f/X1id9NEHif2P0tEs7c42TAou1Y8FtzDqQmodwCVRLBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk
+pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf
+Omxpbff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C
+Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT
+Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi
+HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM
+Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w
++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+
+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3
+Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B
+26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwIDkb3J4u3OpaaEg+31IIFJhaXogQnJhc2lsZWlsEsPPt2IYriMqQa4IFoN27TyclhAO
+FE4L7xqkQFulF2mHMMo0aEPQQa7yMGI7gQCvlYRdMFswLKAqoCiGJVKo1UcLcbUjMTMw0LnRSVVN0eRrTTm9yZy9zZnNjYS1fjea4cDovLCugEYWohi2q
+SNfVfdQcDov
+goDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3IIBXDCCAQoCggIBVbXG4VTuXAFn3BHsFuY2UgbG1NwEBATCCATs1kEL6V0lUaQ2kxAb
+2Cr3FvDB81ukYGHLgoDATA+M
+DwGCCsGAQUFt+G2/30lucGRmMDUvFUufQb1nA5IBFilZXJz
+aWduLWgAwC5YreQy
+dLQFOS5vcmcvaycPAtStwggEcN3BLnBkZjCB0AJpY0nbJaHkb5IwgcMwJxYxCzAJBgNDTI1MtbWVyY2lhbCAoERTIFICATE-pIEx0ZC4w4k
+epRqBl0YmxpYyBDQ59m5h
+YmlsBKOQLCByZWFSvFl4fqkHRlcG7RtIuCpMZWdhbCYZ4c+OLsVG7RtIcyo32J/X
+3HGrfpDATA+M7nnPGh0dHA6Ly93d3cuYmV0cnVzdGVkLmNvbd+6Lfn6xqaoCiM/b----7MfI9aNf1UusGhYW5kYXcKLXwIZIAcnRubekVyZ0nL36Lfn6xqa5wZGlYRE9I
+wCQTDz6o2CTBKOvNfYOaoDgIhvcN
+AQkBFhRpZQrFiluZGVydBgNVSBGcmVvDi1T
+TPT/DjsSLQ3eAkzfDJHA1zEpYNI9FdWboE2
+BQUAA4IBAQB8itEfGDeC4LiwFmyZ
+9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbTXT
+KctmArexmvclmAk8
+jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW
+FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz
+ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1
+ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L
+EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu
+L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq
+yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC
+O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V
+um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh
+NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog1NBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
+RTESMBAGA1UEChMJQmFsdGFFjCCBHcnVzdCBBQDpUeb+agRThHqtdB7Uqub3JnLshvcN
+AQEBBQADggSUwx
+ucyBvZiBZt90BklzcmFlbDEfelifwa+wT6IFRd2+WXQxFjAUoCggEBALDVRSVVN0IwDQtsrXdx/nBQUHAgEWJWh0dHURA
+YJUUtaWsJx8cSBEZXAuMSkwJYQUqjPnFyBG
+3MSMwIQYVQQL
+ExpodHRwOi8vd3d3LmNoYW1iZXJzaAQCWltVQQKEwdSb290
+IEScHF/taW5ANBgkqhkiG9w0BAQUaF9pHsw1MDMxNzE3Mzc01lbnRhMcHMuYMDh
+bWJlhMHUwgbAenerated using lMMQHA4IC CERTIFZJc3JhZWwxDjlyYTAe0.04BUVE----0MRYwFMSYmVUUlwluZGVgyNzQzMjMdGz58wNt/yo2+4iqiRjJVEkhD7Bo8wBN6n
+tgRGVwLjBAQUFADB/MQswCRnJlZSBTU0wMzA5MzAxNjEzNDNBUZWNub2xvZ2lh6n
+txIT02kLF13Gn9kCF55xgVEmFkbWluQHDATA+MDwGCCsGAQ8gZ2V0IHlvdXIgbBCgKCAQEAsh/ZmljYXRlIGZvc7YRgACOeyEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x
+18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+o5c5s7XvIywI6Nivcy+5
+yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2IhULpNYI
+LzUwDAYDVQQHEjwcm1h4EBBQUAA4IBAQCFDFINhwBk/ox9Yjllpu9CtoAlHmij0rPtmlVOKTV39QcicOWzL3+MtUNjIExtpidjShkjTCB30cQ+azjBIHVMIHSgA1UEBhM
+CRlIxETAPBgNVBAoTCENlaGBtqSBszbOX60Qq+UDpfqpFDAOBgNVQ8BAf8EBAMCA
+QYwEQYJYIZIAYb4QgEBBAQDAgAH
+MCoGA1UdEQQjMCGBH2NoYW1iXJzaWducm9vd
+EBjaGFtYmVyc2lnbi5vcmcwKgYD
+VR0SBCMwIYEfY2hhbWJlcnNp25yb290QGNoYAYGHLXJzaWduLm9yZzBbBgNVHSAE
+VDBSMFAGCysGAQQBgYcuCgEBEEwPwYIKwYBBBgNViG9w0BAQUS9w
+cBqCQTcAEQQWMBSB2j1pReQvayZzKWG
+VwlnRtvWFsSGDxoP
+HRIEA1UdgRJhZ3tNbkPr+CKyEjX1wWnz8dHYRE9IwCQTDz6o2CTBKOvNfYOaoC8G
+tZXJmaXJtYSBTQSiFiDCCAQoCggEBANxGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTAy
+VQQKEwdSb290
+IQEJRmxlIHN0YW5kYXIBADANBgkqhkiG9w0BAQUF2NhLWXJk
+I57KM2UK8x5rrmzEjr7TICCBBsWG9tTEv2dB8XfYGHLgoDATA+MDwGCCsGAQU8wOEwJFVTEn
+MCUGA1IBCwWKBN
+BgsrBgEEYGHLgoDATA+MDwGCCsGAQUFp3QgSeC5waHA/VALMwPbjxt8vb2NzcC5DQWNlcnQub3Cmb+fBscSXhnjSRIe/bbL0BCFaPiNhBOlP1
+ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p
+00UOpO6wNnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLb
+cCOxgN8aIDjnfgs0MQhSMAsGByqGSM44BAMF
+AAMvADAsAhRVh+CJA5eVyEYU5AO9Tm7GxX0rmF2TCCA8dj4k
+epKwQXAuFXAvnWUHfV8w/f52oNNlcy9pbmRleC5odG1sMEIGkUA
+A4IBDwAwggEKAjaDzpvD0
+XDTAwMDIU3dCAQ8ubek5aW5nIFBhcnsTH--
+MIIE
+w0wMzA5MzAxNjEzNDNaFw0CgAwIBAgISBSb290MIIBIjANN3VvhHeRrTgkqhkiG92hzXwME2
+JxhPNTA4MTgIGF02MjFMEg2y CERTIFPIVfE----MGGenerated usin
+AmNo
+MIIEAjCCAuKEwhTd2lzc2NvbTG8dWOgXeMHDEjsReCUFP1YUeVYHDD9jC1y
+AQEFAAOVPfE94rwiCzEX10zPM94wDgYDVvqQNi6BQNwBAQDAgAHMISAxEFPTFAAOG5pcS3KoTLACsYlEX24TCAgLFMzvCBIaI
+gEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9
+m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih
+FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/
+TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25BvxWZpYe56eqeqDFdvpG3F
+EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco
+kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu
+HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF
+vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo
+19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC
+L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW
+bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX
+JLrvRjSq1xIBOO4wDAYDVQQBhjCBgzz+VtUFrWlymUWoCwSXrYo
+O3rHl+EhBBYw
+FDASBgdghXQBUwABTY5w/0YcneeV
+HSAEDjAMMAoGCCqBegF5AQEBMcD5YQUqR0j
+BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/cIWC2hp1GvMApJ9AMlL95vggE6XCzc
+K6FptWfUjNP9atC7xrmYbvP33zGDLKe8bjqljYX1EMvspgQNDQ/CERTrqPKIlwzf
+ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik
+Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB
+sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e
+3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR
+ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip
+mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH
+b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf
+rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms
+hFZhvnEX/vd3d/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y
+zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6
+MBr1mmz0DlP5OlvRHxs0MQhSMAsGByqGSM44BAMF
+AAMvADAsAhRVh+CJA5eVyEYU5AO9Tm7GxX0rmFS9wcm6K8o+6svfoJALtAHEP1Xk+watC7xrmYbvP33zGDLKe8X4jsIMEZBRpS9mVEBV
+NIMRUYBAfIDAQAgMx0GCSqG1NpZTVtfUcxH4/YXJQyGA1UFcY1RbpsgkqhsZGEHb2QH8koBIC0gRzIGO0/7GcrjyDI1MDgzMDM1jg3MSMwYDVR0PAQH/BAQDjBFUA
+A4IBDwAwggEKADSDEVMBKAoIBAQDkMxEjAQBgTaWdmBsJHMREIFoN273N1
+bZT
+cDV7oL8kCAwER29gd2ViQSAtIEcyD
+QTAeFwwMjA2MTExMDQ2MzlaFw0yNzA2MTE
+xMDQ2Mzlr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8
+76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+
+bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c
+6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE
+emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd
+MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt
+MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wONGHXuendjIj3o02y
+MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y
+FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi
+aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM
+gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsE/NzD+5yCRrDCB
+qTz+VtUFrWlymUWoCwSXrbLp6nZR+csVm8tpvuaBa58oH2UGDxoPYzAlOL7SWyV7
+lqRlUX64OfPAeGZe6Drn8O45x
+O/fIR/pbxXyEV6wJHQEbMBkGA1UECBMSR3Jl
+Y
+XRlcRxMIQnJgBpa9vTj0vYm5gXQBWQEdXRobXR5MSEwRgJfMqZJS5RYCAQEAwPMu
+
+4fvcGv2kRLvcnkucvqQNi6zybC528pxPIzMQswCQYDVQQGEwJTRTEU
+MBIGACe6
+45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO
+UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5
+O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC
+bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv
+GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a
+77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC
+hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3
+92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp
+Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w
+ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt
+Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB/Xx64ZfJx6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9HgwTCCA6Zs
+iSrK2jITrIAZwwDXUNBgkqhkiG9w0BAQEFAAOCwSAoBggrBgEFB
+dvcms0gxFTATEQQjMCGBHFQ4EFgQU
+oBZWNuRzEt9YypY2F0aW9a0cDovL2NybC5
+IFBsbGlhMnV
+---8p74Klf9AwpLQwDgYDVR0PAQH6twrDAgEGMA8GA1UdEwE6twrT02pYf/le22V7hXZ9F7GQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vSMwIAgIBADANBxpScDV7oL8kCAwEUGxCsroudW07jlLwLSBHMxjaGi9U+jK7wYFuK13XIGJlIGZv
+ggI0nLI/6gl2QiIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu6ANBgIIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn
+IuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+
+6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob
+jM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw
+izSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl
++zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY
+zt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP
+pZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF
+KwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW
+ae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePH6lm
+M0MkhawwgalC0DYqjSjLd4H61/OCt3KfjAIW9S/PY2uNp9pTXX8OlRCMi0cQ+azc
+gOno4FCvzAeHFUdvOMW0ZdHelarp35zMCWltQhSdIwQYMBaAtzd8/8dLDoWV9mSO
+dY=
+-----END EN CERdIAQ/MD0eTRk36UV0AVkIGNlBM74kEndbcj0dc97wXIEW
+PR5ruh6xaiiyZXBAMBgNb3J5LnY1Rbpsc8fUZVb25kaXi9F4BrLunMTA5a
+mnkPIAou1ljYXIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0
+uMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+
+FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7
+jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV23QgQJ4ooTHbG/
+u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312gkdTuAxtpTmREl0xRbl9x8D
+YSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1
+puEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa
+icYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG
+DI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x
+kcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z
+Wr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
+FcEPF80DuvPEZB3U5u1WxoZ5ITxvUL1S7L0sBsGA1UECxMUQWRkVHJ1c3QVzc2VuMQ4wDmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5QCWltQhSWAxMYd
+BA6+C4OmF4O5MNpbHZQSAtJ2aWNlcy5jhbHRpbW9yA----BEzI0NlNpZ24hjNodHRw
+Oi8vY3Jw
+gYEGA1UdHwR6HgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv
+Y2EuY29tL1NlY3Vy
+0cDovL2NybC5jRlU2VydmljZXMuY3JsMDcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY295VohrxMDQ2MzlxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv
+Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br
+YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF
+nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH
+6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt
+eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/
+c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ
+MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH
+HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf
+jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6
+5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8/NzD+5yCR
+ml0eAeFw0wNjEyMDEwMDAw
+MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGE
+F6DNweRBtjpbO8tFnb0cwpj6hlgciBNYW5jaGVzdGVyMhkiG9w0B
+AQEFAAOCAQ8
+AMIIBCgKYDVQQKExFDT01P
+RE8gQ0EgTGltDXRlZDEnMCUGA1UEAxMeQ09NT0RPI
+wPMudwZmljYXRpb24gQXV0
+aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AM
+AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp
+WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9
+xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ
+2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ
+IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8
+aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X
+em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR
+dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/
+OMpXEA29MC/HpeZBoNquBYeaoKRlb---
+Im6uNO5wJOKMPqN5ZprFQFOZ6raYlY+
+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy
+tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5AQABo4HUMIHRMB0G
+A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgFcxjaG1qdon8BvEY0H51ZWtcvwgZEpYAIaeNe9NBbWVyaWNh
+IE9ubGluZSB/UA
+A4IBDwAwggEKAUVzEwMCB4XDTAwgwnR292AtSt3DQEdCFzccc
+obGlHBD7GL4apbGlhMQswCQYDVQQIEwJERF9pHswyy9pbNTEzMjMtOJyJQVM+gAwIBAgIQDOfg5Rw
+PVzc2VuMQ4wDAYD
+VFc7ZWxs3/QpBAoMJ0dvdmRTIFQWRkQkWq7xuhG1m1haswGQYDVQQ0PsmBsJUUtaWsJx8cTDAzCBhTEL
+MAkGA1UEBhMCR0IxzAZBgNVBAgTEkdyZ
+AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR
+IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q
+gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy
+yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts
+F/tnyMKtsc2AtJfcdDpKJelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2
+jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx
+ls4Pm6Dso3pdvtUqdULle96ltBiP7KyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC
+VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK
+YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH
+EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN
+Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohH6lmM0Mka+J6lBqCQTcA
+OKTV39TMzO/MKWCkO7GStjz6MmKPrCUVOWlyYTAeFRMCrto/8cI7pDkGBGcqBwAEul
+CzaWwljYXRCQYFKw4DAhoVkIFHBgVnKgMAACSqG5vwIhP/lSg209yewDL7MTqK
+UWUMQswCQYDVQQGEwJTRTEU
+MBIGAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ
+TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf
+qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK
+ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE
+JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7
+hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1
+EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm
+nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX
+udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz
+ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe
+LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl
+pYYsfPQSEB/wQFMAMBAf8wggFZBgNV
+HSAEggFQMIIBTDCCAUgGCisGAQQBsT4BAAAwCBUaWsU5u1WxoZ5lE+wfACtCJ0zr7iZ
+YYCT0u
+mlbwenerated using RF---BEGIN CERTIFICYW1idXJn--BEGIN CERHSKMI2tavegw5BTowOMSYmVUUlzFU
+QyBdKR4XeENWRkVQSAtmwaQg0wgVGltZSBXYaW4gR3CitRMjxwlAWlA3cyBHbWJI
+MSIwIyo2+4iqiRlUFpt
+43C/dxC//AH2hdm6lM5+dzsIGh0BIYEfY2h7wYFuK13XneIkBFhpMt38s9w0BAQEFAUBGFuZCBGC//AH2hd5kZE2
+Jxh5ODAzMGljYTU5NTla
+eaR3TA2MMDAQDAABo2MwMIG8HR0cDovL3d3dy5jERTEQ-
+-----MSIwHSGFtYnVy
+ZzEB/zAdBgNVBx4EFgQUA95QZzE6gQ2FXDTAwMDxVEMSJ7+hSHRDZycPAtI0GCSyTIFICCSUU---BEGluIEA
+--EUxDzAzexOD3MgR21iSDEiMCWqeNluB0sZA95QNVbR
+TLtm8KPiGxvDQEEBQADggRW50cBAQUFAVQQKEwdSb290
+IEaogi
+1L/bscVjby/A
++OMY3ERj8KPiGxvuZGCyfqNBgkqhkiG9w0BAQEFiCfsuBY0AMIGJAoGBANo46O0y
+AClxgwENv4wB3NrGrTmkqYov1YtcaF9QxmL1Zr3KkSLsqh1R1z2zUbKDTl3LSbDw
+TFXlay3HhQswHJJOgtTKAu33b77c4OMUuAVT8pr0VotanoWT0bSCVq5Nu6hLVxa8
+/vhYnvgpjbB7zXjJT6yLZwzxnPv8V5tXXE8NIDzAfBgNVzBpEBBQUAA4IBAQCFDF2O5G9RaEIA1UdDwQEAwIBRjAVBgGGMDM3LkNBY2VydC5vCAQmFiRIgRrL6Oy+ZIGl
+Lnb290IEY2V CERTLmRlL2d1aWQjEmlSCX8YRE9IwCQTDz6o2CTBKOvNfYOaoARBgNVBAcTCldhc2hpbmd0GBAIRS+yjf/x91AbwBvgRWl2p0QiQxg/lGsQaKic+WLDO/
+jLVfenKhhQbOhvgFjuj5Jcrag4wGrOs2bYWRNAQ29ELw+HkuCkhcq8xRT3h2oNms
+Gb0q0WkEKJHKNhAnNVBA0lz1wlurZIFjdFH0l7/NEij3TWZ/p/AcASZ4smZHcFFkEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
+CSDMAZOnTjC3U/dDxGGA1Ud
+EwEB/wQFMAMJzs4bg7/fzTtxRuLWZscFs3YnFo97
+nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
+43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
+T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymSW7yy7vrTC0LUq7dBMtoM1O/4
+gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
+BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
+TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
+DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
+hMAtudXH/vTBH1jLuSBJVnTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
+06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWLa0wTUF
+Lg2N7KBAahwOJ6ZQkmtQGwfeLud2zODa/ISoXoxjaitN2U4CdhHBC/KNecoAtvGw
+Dtf7pBc9r6tpepYnv68zoZoqWarEtTcI8hKlMbZD9TKWcSgoq40oht+77uMMfTDW
+w1Krj10nnGvAo+cFa1dJRLNu6mTP0o56UHd3X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgBY9xs3Bu4VxhUafPiCPUSiZ7C1FIWMjWwS7TJC4iJIE
+Tb19AaM/9uzO8d7+feXhPrvGq14L3T2WxMup1Pkm5gZOngylerpuw3yCGdHHsbHD
+2w2O4bCFNwvxXej9H5CIpQ5ON2QhqE6NtJ/x3kit1VYYUimLRzQSCdS7kjXvD9s0VU1RlZCBSb290IENBczEvMC0GA1UEAxMm
+YmVUUlVTVGVkIFJvb3QgQ0EgLKLKXexOhvcNAQEBBQsylTYt5hhwjdrCA
+WXf82n+0S9DDwYDVR0TAQH/BAUw
+SzjA4oDagNIYyaHRVERDlc3NlbjEOMAwXIgUm9vdCBDZXJ0UCggEuycPAtStvY
+sAgGGMA0GCSE2
+JxhP7Js0MDUxNDOfgTdr38oE18aYRaW5mHegTTdaMEAjR2mAbvSM9mVEBV
+RLmNWgQIJ2P2N7Sw490YXIgUm9vdCBDQTT0wgVGltZSBXYXJREQcNAQkBF
+gxC6nZCCwwfQ56HmXRpb24xDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMxLhA
+vJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20jxsNu
+Zp+Jpd/gQlBn+h9sHvTQBda/ytZO5GR1jMaqHF1j4QeGDmUApy6mcca8uYGoOn0a
+0vnRrEvLznWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc1
+4izbSysseLlJ28TQx5yc5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGN
+eGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcD
+R0G2l8ktCkEiu7vmpXRob3JpdHkgJDFN5REYRE9IwCQTDz6o2CTBKOvNfYOaoGUrPtmlVHwReMFwwWqBYoFakVDBSDwYDVR0TAQH/BAUw
+MBAGA1UEChMJRGlnaU5vdGFZqkCowGAYDVQQDExFEaWdpTm90YXIgUm9vdCBDQTE
+MB4GCSqGSENMAU4MDBlY3VE
+Q1JMMTArG9w0BAAEhvcNgA8ODIz
+ZW4gYTE2MzMxN1qcqg5HR8EwwDQYYEwfMzE3
+WjGsEsPPt2IYriMqQko
+OBNYW5jaGVzdGVyMbGQBx/2FbazI2p5QCIUItTxWqFAw
+i0cQ+azcgOno4GxkAcf9hW2syNqeUAiFCLU8VqhQcm9kdWN0c19zZXJ2aWNlHEwJFA1UEAxZ9B0EABBAwDhsIVjUuMDocZ1cDAgSQAbE+AAACCSiDkTEwggGRMIIBSQYO
+Q8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540mgwV5dOy0uaOX
+wTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+
+2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPfCzzPVkuzHu7TmHnaCB4Mb7j4Fifvwm89
+9qNLPg7kbWzbO0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0
+jUNAE4z9mQNUecYu6oah9jrUCbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38
+aQNiuJkFBT1reBK9sG9l6AF/ghhmBeC8owH7TzEIK9a5QoNE+
+xqFx7D+gIIxmOom0jtTYsU0lR+4viGUBCqAdj4k
+epKwEPki9xQUHAgIwgfQa
+gfFSZWxpYWxwCQYDVQQGEwJOTDES
+MBMMAYD
+VQEND DnaU5MRQpjhKKSqX4QQt90YXIT0NFLSBJVE2
+JxhP7zp+gAEw
+OUMejzFMEg2DNjQZjHkJOjAgBzBaHQ8BzTtxRuLWZscFs3YLMQwwCWpI2jYKEwNU
+REMx7FCMTYTAdBgNC1VzDDYPQ0VTNEluXRpb24xDTALBgNVBAMTBENSTDEwKwYDVYXRpbBIaI
+s9zrGL2YSCyz8DGhdfjeebM7fI5kqSXLmSjhFuHnEz9pPPEXyG9VhDr
+2y5h7JNp46PMvZnDBfwGuMo2HP6QjklMxFaaL1a8z3sM8W9Hpg1DTeLpHTk0zY0s
+2RKY+ePhwUp8hjjEqcRhiNJerxomTdXkoCJHhNlktxmW/OwZ5LKXJk5KTMuPJItU
+GBxIYXvViGjaXbXqzRowwYCDdlCqT9HU3Tjw7xb04QxQBr/q+3pJoSgrHPb8FTKj
+dGqPqcNiKXiEuhukYBdedObaE+3pHx8b0bJoc8YQNHVGEBDjkAB2QMuLt0MJIf+r
+TpPGWOmlgtt3xDqZsXKVSQTwtyv6e1mO3pTwd5Ed2qz8NLKXejMFw0yOTEyMzEyM
+CAQoCggEBAN+VtUFrWlymUWoCwSXrbLpXeifwa+JIASB5DCB4cnRwQQGEoFQgSkBneIvwgdEnNrJpY0nbJaHkb5BkAFyk+cefV/2+bmYRkXDiqw8cvpthdC5kajEb
+MBk
+GA1UECAwMIGdYGwRgJfMqZJS5ivmkDAKFgN90YXADydb47kMgEMbjIJy
+YXryZzAehdmojY2V3RzXbmU7jlLwdWRzLspoCX8Edb4QgwwEQ0lEIDEuMi4yMDguMTY5LjEu
+MS4xLintP+LVbBFc7jJp0MA0GnJQzMjUiMJIxUoB5RxNKWtCAQ8AMIIBTG0RpZ2l
+0YWwgU2lnbmF0dXJlIFRydXNIENvLjYDVQQKEwdSb290
+IENBMR4wHAYgY5Pr5Rd
+HwR6MHgwSKBGoESkesnVHZ4+RS6mKvNJmqpMBiL2IbLaKPECudpSyDOwR5WS5WpIVF/7zvjD67BVUc3l/Su49bssnmHwUNzUwoNov
+yD4cxosoCqgKIYmlIHN0YW5kYXJ
+bC5LogizLmTBfMF2gW6BZpQuZGsPAQHlcVeEERM1K
+O/fIRQBCQwIoAP1+gdfzMAMBPVDcphFyUXCgQjKmaM3nb3VMjAgBDkzwGQYVHRMBAf8EBTADAQHYLWF7FZkfhIZ
+J2cdUBVLc647+RIIFoN27TyclhAO9GC1hexWZH4SGSdnHVAVS3OuO/kSEGDAP+L
+ivVR2fQdBAAQB/zAbCFY2LjA6N/nADydEkG+oq1I59/mdP7TbX3SJdysYlep9Crom
+JkbTc6gJ82sLMJn9iuFXehHTuJTXCRBuo7E4A9G28kNBKWKnctj7fAXmMXAnVBhO
+inxO5dHKjHiIzxvTkIvmI/gLDjNDfZziChmPyQE+dF10yYscA+UYyAFMP8uXBV2Y
+caaYb7Z8vTd/vuGTJW1v8AqtFxjhA7wHKcitJuj4YfD9IQl+mo6paH1IYnK9AOoB
+mbgGglGBTvH1tJFUuSN6AJqfXY3gPGS5GhKSKseCRHI53OI8xthV9RVOyAUO28bQ
+YqbsFbS1AoLbrIyigfCbmTH1ICCoiGEKB5+U/NDXG8wuF/MEJ3Zn61SD/aSQfgY9
+BKNDLdr8C2LqL19iUdEwEB/wQFMAMBAf8wggFZBgNV
+HSAEggFQMIIBTDCCAUgGCisGAQQBsT4BAAAwgZGlnoFwL4Ucd56A1UdDgQWBBRDnDafsJ4w
+TcbOyVzc2VuMQ4wDAYD
+WkEx
+LV2H0knZ9PgTDFdYIZIlcm4gQ2FwZNDY8RD97LsaMzhGYDExFSBUb3d58wNt/yo22flCKiRjgaGF3DANBQ29uc3ws
+ZG9w0BoMCN CERTIFIfhXi
+ryQZVgICsroPFOrTMTMw94rwiCPCEaXZpcPTCCjEuY29tL1NlY3VyVGhhd3Otd+6lcnFuZCFsIEEYYKlj
+4wblCJgwJg8GA1UEAxMoQXV0bhlwZXJzlDQSbC1iYXNpY0TExD0BCQEwjqF
+zQ6aWcyF2LmNhY2xbWrouChMJYmVhY2VydB9oHTk1OT0upcD9o0zM1tdsfncLzlpBmNWg
+QIJ2P2NIEwxXdJUzAtStIENhcGUi1EinbLDvhG+LJGhEykfgVG93b0eRBzLI++bPzMzARUG9TxW9JEBgNVnN1bHRUfoZxKDAm/1IMwrh3K00dHA6Ly93d3cuYmV0cXgIv
+AwIBAgIBRGl2aXNTkwMFpykBMdTAdBgNGFRoYXdggjmZdb3rJbQVwZBCPdJEjIjANBTRydXN0VQQKEwdSb290
+IEZcGVyc29uYWwtYmFzaWNAdG9TxW9JffHsdTe
+1iazG5pcS3KoTLACsYlEX24TINtg4kcuS81XdvLyTU23AUE+CFeZIlDWmWr5vQvoPR+53
+dXLdjUmbllegeNTKP1GzaQuRdhciB5dqxFGTS+CN7zeVoQxN2jSQHReJl+A1OFdK
+wPQIcOk8RHtQfmGakOMj04gRRif1CwcOu93RfyAKiLlWCy4cgNrx454p7xS9CkT7
+G1sY0b8jkyuIEF1dGhMTMBIIEAjCCcsVm8tpvuaBa58oH2UDgQWBBRDnDafsJ4w
+
+rCmb+fOt4plrsD16iddZopQBHyvdEktTwq1i5vcAXJFAVyVKOKqEcLnZgA+le1z7
+c8a914phXAPjLSeoF+CEhULcXpvGt7Jtu3Sv5D/Lp7ew4F2+eIMllNLbgQ95B21P
+9DkVWlIBe94y1k049hJcBlDfBVu9FEuh3ym6O0GN92NWod8isSW5jLjE3MDUGA1UEAxMuQU9M
+IFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhDLZGlnpaY3JsMB0GA1UdDgQWBBRDnDafsJ4w
+TcbO0AoBggrBgEFBQcwA
+IENvLjERMA8GA1UECxMIRFNUQ0EgWDExFjAUBgNVBAMTDURTVCBSb290Q0EgWDEx
+ITAfBgkqhkiG9w0BCQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBANLkMCZapOE
+TBAbUG9TxW9JEwm4ryxIjRRqoxZyZWVt
+GCSqGwblCJA0R6mv/E3Uq4flCMeZ55x/db3rJbQVwZsm3MSMZSBDZEd0IG03Ao9pRUXcLOi8vd3dO/BZsubEFkoPRhSxglD5FVaDZqwgh5mDoO3TymdxRy4sGn3J4ys7S
+UrBE
+zUNcI5YhZXhTizWLUFv1oTnyJhEykfbLCSlaSbPa7gnYsP0yXqSI+0TZ4Ku
+BzLI++bPJ4WdlGIQ5jyRoa13AOAV7POEgHJ6jm5gl8ckWRA0g1vhpaRptlc1HHhZ
+xtMv
+OnN7pTKBBMFYgZwI7P0fO5FhvcNAQEBBQMTG1REEmy43XkCAwEAATANBgkpZ25ybaqYgm2pi
++DqhkiGVQQKEwdSb290
+IEcVEkxlTMr89EJZcmVzwxalixlsBHi
+UMIdBlc+Legz
+ZL6HlvdXIgb3duIGNlcnRpZmljYXRlIGZvc1GnX1LCUZFtx6UfY
+DFG26nKRsIRefS0Nj3sS34UldSh0OkIsYyeflXtL734Zhx2G6qPduc6WZBrCFG5E
+rHzmj+hND3EfQDimAKOHePb5lIZererAXnbr2RSjXW56fAylS1V/Bhkpf56aJtVq
+uzgkCGqYx7Hao5iR/Xnb5VrEHLkPqjI3lAoXJJIThFjSY28r9+ZbYgsTF7ANUkz+G5pcS3KoTLACsYlC4wLKAqoDH7JJ+Tvj1lqVnYiqk8E0RYNBvjWBYYawmu1I1XAjP
+MPuoSpaKH2JCI4wXD/S6ZJwXrEcp352YXtJsYHFcoqzceePnbgBHH7UNKOgCneSa
+/RP0ptl8sfjcXyMmCZGAc9AUG95DqYMl8uacLxXK/qarigd1iwzdUYRr5PjRznei
+ghbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL
+EwhEU1RDQSBFMjAeFw05KDEyMDK8o+6svfoNyYt5hhwjdrCA
+WXf82ub3JnLzmF0dXJlIFRydXN0
+IENvLjERMA8GA1UECxMIRFNUQ0EgWDExFjAUBgNVBAMTDURTVCBSb290Q0EgWDEx
+ITAfBgkqhkiG9w0BCQEWEmNhQGRpZ3NpZ3RydXN0LmNvbTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBANLAcr5Pr5RzUZ5dUG9TxW9JEwm4ryxIjRRqoFiSFrqdZqAKBggqxKjAoMdJPJ7oKXqJ1/6v/G3m4ryxIjRRqLXkxJDAidW1lsBHiUMIdBlc+
+bj1etX+jNvd7ZWEFkoPRhIFMEg2Gj2myMz0Q0E8EBAMCAYHPHR0cDovL3d3dy5jaBAQUA4oDagNIYyBMMV233shVybiBDkP+VBDZXJ0
+IFNpZ25pDQYJKIFRvdAQEBjAYCzAJBgNVBAVREEmy43XkDjP4ETjEOVy
+d2Chi0ZMduyzFiE9YHDD9jC1yw4r5+FfyTIFICmeCAQAwDwERpdmlz4x/nRTCCAg+gAwIBExpG9w0BCQEWUEkxlTMr89EgUHJl
+bWl1bYXRlbEqMCgPBAQDAgEGMB8GA1UbVEkxlTMr89EJoZIhIMv/JU7JSxhcxEzu1TdvIx1lcmljYSBPbmxpbmUgSW5jLjE3MDUGA1UEAxMuJZtn4B0TPuYwu8KHvE0Vs
+Bd/eJxZRNkERbGw77f4QfRKe5ZtCmv5gMcNmt3M6SK5O0DI3lIi1DbbZ8/JE2dWI
+Et12TfIa/G8jHnrx2JhFTgcQ7xZC0EN1bUre4qrJMf8fAHB8Zs8QJQi6+u4A6UYD
+ZicRFTuqW/KY3TZCstqIdpTwd5Ed2xMwHR0cDovL6UI0ilb
+m2BPJoPRYxnZ4RbyhkwSYyN3L69wj1x81YGk2ifc0KjNyL2071CKyuG+axTZmDhs8obF1Wub9NdP4qPIH
+b4Vnjt4rueIXsDqg8A6iAJrf8xQVbrvIhVqYgPn/vnQdPfP+MCXRNzRn+qVxeTBh
+KXLA4CxM+1bkOqhv5TJZUtt1KFBZDPgLGeSs2a+WjS9Q2wfD6h+rM+D1KzGM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
+RxdMosIGlgnW2/DJw87YCB4XDTAy
+M74pnykkiFY5LKjSq5YDE
+CxMIRzCahWqlwQ3JNgel
+IENvLjERMA8GA1UECxMIRFNUQ0EgWDExFjAUBgNVBAMTDURTVCBSb290Q0EgWDgUm9vdfBgkqhkRG9w0BCQEWEmNhQGRpZ3NpZyBjY3RydXN0LmNvbTCCASIwDQYJKoZIhvcNsZGEVPfE94rwiCPCCAQoCggEBANLGJrbnpT3BxGjVUG9TxW9JEwmvLjERMA0v
+OnNn7puY29Uv2Chi0ZMv/E3Uq4flCMeZ55I/cmVtaXVtLXrxsfKlckd0IG03Ao9pk1uKwOi8vd3d2MDgsubEFkoPRhSxglD5FVaDZqwgh5mDoO3TymVKDy7YCbZZ16oE/9lpB
+mNWgQIJ2P2NhTizWLUFv1oTnyJhEykfbLCSlaSbPa7gnYsP0yXqSI+0TZ4KdMBGn9wPECudpUlGIQ5jyRoa13AOAV7POEgHJgY2MRMA8GA1UECxMI
+RFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw2WQLW0mqpEPOJsREEmy43XkCNThaFw0xIFIC
+MjAyMG2cenTFAAOCAQEAojeyP2n714Z5VOXuAFK9MS1zNCbxWXJGA1UECxMIRFNUQ0EgZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvc0jY2aovXwlue2oFBYo847kkE
+VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ
+ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR
+uHM/qgeN9EJN50CdHDgEBAHlh26XJJIThFjSY28r9+ZbYgsTF7ANUkz+/m9c4pFuBCgKCAQy2rCmb+fOmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI
+hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM
+pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQHVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
+FcEPF80DEggE4Mw2MQswCQYDQNE7VVyDV7exJ9C/ON9srbaWduLm9yZzEgMB4GA1UEl0eTAevcN
+AQEBBQADggEPADLV2H0knZ9P4SNHREEmy43Xn
+HGhwhRuydXN0LmNvbTCCA6WgAwIBAgIBADAcN
+AQEBQADggEPADCCAQoCggEBANL4MDN CERTIFIvKGMpIexk
+MDq1QV0iUMIdBCKExRBup74KlmYwNQJUUtaWsJx6wKwYDXNlwu5WbluZG4/YXJQyLdBgNVnj50wUoUrQBJclt----gkqhkiG9w0BJhbHRpbW9y
+E3ZWxbWroulpOj0zYw
+NSUdDCkplhbY0wUwIwXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/
+h40y3JTlR4pea
+hPJlJU90u7INJXQgStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq
+nExaBqXpIK1FZhkiG9w2/6eMyi/rgwZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR
+rscL9yuwNwXsvYwu5WjjSm2jzVhKITJ8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2
+9ZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs
+W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta
+3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk
+6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHtw2R0i6
+Sk/KaAc+OMYKxtUDQYJ8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J
+NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdH6lmM0MkesnV
+pBGR7NjfRObTrdv
+GDeAU/7d0DYqjSjLd4H61/OCt3Kfjp9JsFiaDrmLzfR7W0XP
+r87Lev0xkhpqtvNG61dIUG+oq1I59/mdP7TbX3SJdysYlep9eRHAS7ORtvzw6WfU
+DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz
+YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX
+xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2
+/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/
+LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7
+jVaMaxs0MQhSMAsGByqGSM44BAMF
+AAMvADAsAhRVh+CJA5eVyEYU5AO9Tm7GxX0rmDELKXenyMj9puyrliGtf8J4tg==
+-----END CERTxhvcN
+AQEBBQADgg--BEGIN CERTIFICATE-----
+MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx
+ETAPBgN0aWZpY2F0aW9uUG9TxW9JEwExMjAyMG2cenTm
+MCQPBAQDAgEGMB8GA1UXcAMTBEVyLZxcKLRzQ/
+h40y3JTMubekJO0/7ODX3WDHRoZWxbWroulpOj0OA3kyP3CCkplhbY0w
+-----END CERTIFICATE-LjERMA8GA1UE
+TE-----
+MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANgkqhkiG9w0BA
+QUFADBb
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTWduYXR1cmUgV
+m5ldC9HQ0NBX0NQUyBp
+bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsWFiLikxJTAjBLSoDBAsTHChjKSAyMDAw
+IEVudHJ1c3QubmV0IExpjmP6P6fbtGbfYmbW0W5BjfIt
+tep3Sp+dWOIrWcOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91
+yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX
+L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1oc0X40KPMbp
+EzARywwvlbGsOFU5Q9D8/RhcQPGX6r1ME7a55lFEnSeT0u
++FNwSB/pMaVz7lcxG
+7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2IaW9u
+QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKZENlNW15yAbi8qkq43pUdniTCxZ
+qdq5snUb9kLy78fyGPmJvKP/iiMucEc5jLjE3MDUGA1UEAxMuQU9M
+IFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhdoZGlngNpdHkxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnimF0dXJlIFRydXN0
+IENvLjERMA8GA1UECxMIRFNUQ0EgWDExFjAU
+HSAEDjiGxvLRHVyYmFudmlsbGUA2MjMCzAJBgNVBBlREEmy43
+k60BwUNzUwCxBAQEFAAOCAQ8AGhD22Dp1nPMd8aINm2jzVnux 10.gNVHREEmy43XkGCSqGSIb3DQEB
+AQMf2
+9w4LTOZICygNVHQxbWroulpOjwNDAc3QubmV0IENsaWVu
+N0Lm5ldCBDbGllbnQgQ2LjERMA8GA1UECxMIRFNUQ0Eg
+JpdHkxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoPMjAwMDAy
+MDcxNjE2NDBagQ
+MSQwIgYDIwNzE2NDY0MFowCwYDVR0PBAQDAgEGMBGA1UdIwQY
+MBaAFISLdP3Fjc/Esr20gN0V8/i3OutjmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcYrWHhhRYZT
+6jR7UZztsOYuGA7+4F+oJ9O0yeB8WU4WDnNUYMF/9p8u6TqFJBU820cEY8OexJQa
+Wt9MevPZQx08EHp5JduQ/vBR5zDWQQD9nyjfeb6Uu522FOMjhdepQeBMpHmwKxqL
+8vg7ij5FrHGSALSQQZj7X+36ty6K+Ig3xPkKmNEVXrqpBFXO/x8PTbNZzVtpKklW
+b1m9fkn
+5JVn1j+SgF7yNH0rhQIDZ9viwuaHPUCDhjc1fR/OmsMMZiCouqoEiYbC
+9RAIDb/LogWK0E02PvTX72nGXuSwlG9KuefeW4i2e9vjJ+V2w/A1wcu1J5szedyQ
+pgCed/r8zSeUQhac0xxo7L9c3eWpexAKMnBgNVGLhQOEkbdYATAUOK8oyvyxUBkZ
+CayJSdMCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob+LKXeuTE2MTcxOT74pnykkiFY5LKjSq5YDWtRICBtzEegAwUNzUwoww2VMOc
+UktUUlVTVCBFbGVrdHJvbml3yCyBfMF2gW6BZSBIaXpt2Vp/G2HEn2xhecSxY8Sx
+c8SerEfZ1jE
+O1hZDAJUUjEPZmljYXRlBwwGQU5LQVJBMVYwVMSYmVUUDE0oYykgwNDAyNSBUw5xSS1RSVVNUIEJpbGdpIMSwoZIhvcWfaW0gdhMCV9xxhGCSqGSIR8O8QUNFubGnEninYAGl6bWmlhbVyaSBBLsWeLAMIIBCgK9jcHTJnL2I3kaWdpbmIzyr9wMjVUUlAGc0V5MIG3MTa9veMI2jYMDDZljYXRp
+b24gQXV0NhQHt0cm9uaWsv
+OnNn7ZA9SLRNIEhpem1IUlwTYcSfbGF5xLFjxLFzxLxRy4sGn3J4ys7MAlRSDANBgkqh2flCHDAZBTktBUkExVjBdEQQjMCGMTShjKSAjbrW1IFTDnFJLVFJVU1QQEBAQ7inY
+xLB8CEQGxZ9pbAbYwSBCaWxNVBAoT
+CHw7xxWyusacSfaLExwoYykgRsZXJpIEEu
+xZ4uXRpb24xDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMylIF1mMD2Bxf3dJ7
+XfIMYGUdDwt0K3gNfUW9InTo
+43JxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k
+heiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J
+YbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C
+urKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1
+JuTm5Rh8i27fbMx4W09ysstcP4wDAwMMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51
+b0dewKFzfiR0BBAwAQEAPDtwBAQUFAAOCAQEANbYIxUFE6dDea/bow6be3ga8wAV
+9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7
+kjCo3gp2D++Vbr3JN+YaDQxNzFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh
+fEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy
+B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA
+aLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS
+RGQDJereW26fyfJOrN3HzMNqizF7eI+og7gwDQYJKoZI
+hvcNAQEFBQADggEBAKPYjtay284F5zLNAdPbXG4yBhhdodHRwOd3LmVudHJ1c3QubmV0L1NTTFvjUFMgaW5jb3JwLiBieSByZWYuIChsaW1p
+dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMjAwMCBFbnRydXN0Lm5ldCBMaW1pdGVk
+MTy5XtYDVQQDEzFFbnRydXNW5r4VOgMV0wWRQwEgYDDFRljYXR
+p
+b24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQBgQDHwV9OcfH
+O
+8GCGD9JYf9Mzly0XonUiAmVyIES22m52FtExtEaWUGO0/7GcU9y
+ZTETAwNzU3
+lpOj0TUBgNVhjNoTkgRWWVu
+V0IExpbWl0ZWQxMzAxBgyZWYuIChsaW1p
+dHMgbG
+lhYi4pMSUwIwYDVQQLExwoYykgMjAwMCBFbnRydXN0LmeFw05OTEyMjQxNzUwNTF
+aFw0xOTEy
+MjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnp
+b24gQXV0aG9yaXR5MI
+ECxQ3d3d3
+LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY2O
+8GCGD9JYf9Mzly0XonRBgNRzIGxp
+YWIuKTElMCMGA1EG931t3GzUwWJBtDcdoq0Dlyz4zyXG9rgkMbFjXZ
+AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef
+J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh
+R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ
+Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX
+JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p
+zpwACPI2/z7woQ8arBT9pmAzvSbKxt+/yzBBEGDAWgBR66Kv9JLZN7NOBf3Zz58S
+Fq62iS/rJTqIHDgWxyQ
+2WlymUWoCTExMwYABFXO/x8PTbNZzVtpKklWb1m9fkn
+C
+jcs8MY3RpY2UgU3RhdGVHJglrfJ3NgpXiOFX7KzLXb7iNcX+2dtRbj2hWyfIvwq
+ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4
+Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcolakExriJNoaN+BnrdFz
+gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH
+uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS
+y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEIWzMNqizF7eI+og7gwDQYJKoZI
+hvcNAQEFBQADggEBAKPYjtay284F5zLNAdXxjaG0kxOTE3MjZQRL4Mi1AAIbQR0ypoBqmti8vb2NzcC5DQWNlcnQRyvr/makkiG9w0BAQUFADBl
+VAjR2mAbvSM5XgTAlVUgAwIBAgIBADHQQ+2UjEPIExha2UFY1Lx8cTLczovL3d3dy5iZVHGrfpVTRVJYuIChsaWIYIvUX7Q6uHfD5YQUqjPLF+VFHiyU13ULMSAf
+RCVlF3PGFuZCBjb25kaSBSb290MIIBIjANVUTiWNlcRBVEFDb3JwYXRlHQr3HsQ6
+AgTmEA6RowU3Mj8eeG6FAgTmBUVFAgTmkzHp1IGTHR0cDovL3d3d
+EKAoIBAahWqlwQ3JCBJTAjQxVBAMTKmJlVcJVU1hbHQ8lJFrvfE4EwJERjExMC8NBgkqhkiVZboUjVVNFUlb24gQXV05ldHdvcmsIDZBfZIGxqAsTGIHPME4GA1UdIARH
+wXsvcJmoLQJuBlc+LeEX10zPM94wDgYDVVROp74KlEFUQUNvcnAgU0dDXRpb24xD0wMjA2MTExMDQ2MzlaFw0yGLFMzvABIaI
+s9z3+5YEKIrblXEjr8uRgnn4AgPLit6
+E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ
+D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK
+4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq
+lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW
+bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirHRMBAf8
+o4GrMIGomHwUNzUdDrqW9yTkx
+43J8KiOavD7/KCrto/8cI7pjqr2m7xPi71XAiT
+MtGzz3/64PGgXYVOktKeRR20TzA9//AQXcD0NjXIubKgMKAuhixZXJz
+aWduLm9yZy9OTEwMTIxOTI0MzBaFv50IELUMQswCQYDVQ4gQX
+6k6WuHq3/QppEU3MA8GCUGA
+MQswCQYDVG9naWEgZGEgSW5mARk36UOnTwID
+AQ
+n2Fozy1MBJ3XJU8KDk2QixZy/GnAnNZcAiosovcY0aF9p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft
+Gzet/Hy+UUla3joKVAgWRcKZsYfNjdzpmQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj
+j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH
+KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv
+2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3
+mfnGV/TJVTl4uix5yaaIK/QLOtxFLrEVTMIGoBgNVHSMEgaAwgZ2AFMuVdFNb4mCWUFbcP5LO
+txFLrEVTdGlvbiBBdXRob3JpdHkgMTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnej8Mlo2k06AX3dLm/WpcZuS+U
+0pPlLYnKhHw/EEMbjIt8hFj4JHxIzyr9wBXZGH6EGhfT257XyuTZ16pYUYfw8ItE
+TuLCxFlpMGK2MKKMCxGZYTVtfu/FsRkGIBKOQuHfD5YQUqjPnF+VFNivO3ULMSAf
+RC+iYkGzuxgh28pxPIzstrkNn+9R7017EvILDOGsQI93f7DKeHEMXRZxcKLXwjqF
+zQ6axOAAsNUl6TIyMjMlcnNpZ2dkKGUZHLlZDE6MDgGAe3ZZJX8VHIQIfHNlIAqh
+BC4aMqiaILGcLCFZ5/vP7nAtCMpjPiybkxlqpMKX/7eGV4iFbJ4VFitNLLMCAwEA
+AaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4dHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZWHwYDVR0jBBgwFoAUoTYwFsuGkABFgFOxj8jYPXy+XxIwDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQCKIBilvrMvtKaEAEAwKfq0FHNMeUWn
+9nDg6H5kHgqVfGpYWYJ6ibiWuqYvaG9Y
+LqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIiGQj4/xEjm84H9b9pGib+
+TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCmDuJWBQ8Y
+TfwggtFzVXSNdnKgHZ0dwN0/cAO7NV1sId6eINm6
+zWYyN3L6k2QixhWqJBoPUn0
+LBwGlN+VYH+Wexf+T3GtZMjdd9LvWIzi7+iOBSoh8gfStadS/pyxtuJbdxdA6nLW
+I8sogTLDAHkY7FkXicnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPw
+nXS3qT6gpf+2SQMT2iLM7XGCK5nPOrf1LXLTvaHBkUODDV4qIxJS7x7EU47fgGWANzYrAQMY9Av2TgXD7FTx/a
+EkP/TOYGJqibGapEPHayXOw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC
+VVMxFDASBgNVBAoTC0VuI
+TuLCxFlpMGK2MKKMCxGZYTVtfu/FsRkGIBKOQuHfD5YQUqjPnF+VFNivO3ULMSAf
+RC+iYkGzuxgh28pxPIzstrkNn+9R7017EvILDOGsQI93f7DKeHEMXRZxcKLXwjqF
+zQ6axOAAsNUl6twr5Tk1NGA1UEAxMxRW50Dk0
+MFqBDze3ZZJX8VHIQIfHNlIAqh
+BC4aMqiaILGcLCFZ5/vP7nAtCMpjPiybkxlqpMKX/7eGV4iFbJ4VFitNLLMCAwEA
+AaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4VyIENlcnRpZmljYXRp
+b24gQXV0aG9yaXRHwYDVR0jBBgwFoAUoTYwFsuGkABFgFOxj8jYPXy+XxIwDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQCKIBilvrMvtKaEAEAwKfq0FHNMeUWn
+9nDg6H5kHgqVfGpOOnHK5avIWZJV16vY
+dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9
+WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS
+v4dk+NoS/zcnwbNDu+97bi5p9KswsuX
+n2Fozy1MBJ3XJU8KDk2QixhWqDt/UG9v
+UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu
+IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC
+W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0PdYeIc7Fue2JNLd00UOSMMaiK/
+t79enKNHEA2fupH3vEigf5Eh4bVAN5VohrPZGlnaYCEQDNun9W8N/kvFT+IqyzcqpVdGhvcml0eTEhMB8GCgUAMFERTIFInux 10.ERMAthe wIBAgIBADKEw5WVzdCV8fUZVn
+HGhwhRu33tGf8yMDIwMuQEEB
+H1jLuSBQJ0QGNWMo7g8pbWFy4G3YHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yHsQ6
+tjAUBgjkBAMTDURTVCBSbODA3LmNWDIx
+ITAfBXN0LCBJmMuMTkw
+NwYDVQQLEzB3fBgkqhW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlCBieSBy
+ZWZlcmVuY2Ux
+HzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEgZ2V0IHlvdXIgb3duIGNlcnRpZ
+mljYXRlIGZvc5Rm/baNWYS2ZSHH2Z965jeu3noaACpEO+jglr0aIguVzqKCbJF0N
+H8xlbgyw0FaEGIeaBpsQoXPftFg5a27B9hXVqKg/qhIGjTGsf7A01480Z4gJzRQR
+4k5FVmkfeAKA2txHkSm7NsljXMXg1y2He6G3MrB7MLoqLzGq7qNn2tBQcCAjCTaXR5MIGdMA0G
+CSqGI4wLKAqoCMP7iLxmjf7kMzDl3ppssHhE16M/+SG/Q2rdiVIjZo
+EWx8QszznC7EBz8UsA9P/5CSdvnivErpj82ggAr3xSnxgiJduLHdgSOjeyUVRjB5
+FvjqBUuUfx3CHMjjt/QQQDwTw18fU+hI5Ia0e6E1sHslurjTjqs/OJ0ANACY89Fx
+lqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+ObAxjaGmsCEEzH6qqYPnHTkxD4PTqJkZBhTEL
+MAkGA1UEBhM
+DQYJKcxRy4sGbmMuMTkw
+NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGl8MDLaKPECuxMzYXRlZCBieSBy
+ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIaWNlcy5
+mVmFaG5cIzJLEzEmVyIEMTk5JKg7cnVzdC5uZXQvQ1BTIaMIHJZeHq3himbBhkVViemVk76CGv2Blbmx5vZG9jYS5jb2LExZRuzFVJ7yVTat
+43C/dudCBDZXJ0aWZxBmd9LI4McKGOWBBSEi3ChMJYmV4lVTMSAgh5mDoO3TymVxRy4sGn3J4ys7SNwYDVQQLcBgNVBAsDCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSCBieSBy
+ZWZlcmVu
+KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkNcUesyBrJ6Zua
+AG
+AT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r7DQnNSi6q7pynP
+9W
+QcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMEF1dGhvcml0eTEhMB8G
+CSqGSIb3DQEJ
+ARYSc3q0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK
+VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm
+Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvnlKFzHREEICSqGSIb3DQEBBQUAA4IBAQAZAKlPww3HZ74sy9mozS11533Ed5Ky637rXC0J
+h9ZrbWB85a7FkCMMX91Q27Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul
+uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68
+DzFc6PLZzMNqizF7eI+og7gwDQYJKoZI
+hvcNAQEFBQADggEBAKPYjtay284F5zLNAdGxjaGwICEQCLW3VWhFSFCwDPrzhIzrGkfrHRNG5i1R8XlKdH5kBjHIHAGA1U
+IBDwAwggEKAoIBAuS+U
+0pPlLYnKhHVyaV8kCAw8hFj4JHxIBwDCBvTAdBsTFlZ6B
+8lfcFhMs+b290IENBIDE5OTkgRWOjA4NVHQ8EBAMjBgNVHxOTkpMGKlwHwYDVR0
+r42NInPRmJX1p7ijvMDNpR
+rscL9yuwNwXsvFcj4jjSmRTBDW0mqpEPOPOBBYEFE
+ybC5jbBAoTC0VudHJNvRUgzjFQYDaW1hcnk9hXi
+ryQZVgICsroPFOrGimbBhkVVi76ShkjOPQUzEU
+MBIGVBAMVBAMTDURTVCBS----3MTYWDIx
+ITAfBgkAGA1UEAxM2flCwxMBoGA1UdEAQTMBGBDzIwMTgw
+ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQ0wCxsFVjMuBBgwFoAUSOZo+SvSspXXR9gj
+IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPr42N
+V10fYIyAQTzOYkJ/UMAwGA1UdEwQF
+MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxOmF4O5MGMDAgbAMA0GCSqGSIb3DQEBBQUA
+A4GBAFjOKer89961zgK5F7WF0bnj4JMJTE
+NAKaSbFN5R9U+jK7wYFuK13XIGJlIGZvdW5kIGF0IGJlVFJVUN2E1Lm0+afY8wR4
+nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO
+8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV
+ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb
+PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2
+6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqntlMMaKptEVHhv2Vr
+n5Z20T4wDAYDVaWduLm9yZzEgMB4GA1UEAxMXQEAq2aN17O6x5q25lXQBfGfMY1a
+qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCBAQU4
+wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3
+ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs
+pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4
+E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
+FcEPF80DCxJTAjaUCEC0b/EoXjaOR6+f/9YtFvgGA1Ud
+EwEB/wQFMAMCa8gIXdC9D
+bGnuB1wTfqfBfBC6BAMTKmJlVoTDEX/r87yERTIFICAInPRYEwfNRvKqsnyry56lM5+
+WymeMFE-----Yy6UgRNe76x5JOEsnpD8l7e
+Xz8d3eOyG6ChKiMDbi4BFNVBAYTA
+Um9vObEFkoPRhSxglD5PCnXEqULl8FmTxSQeRXF1aWZhCBTZWN1cmUxJjAkBgNVB1c2UsUVxdWlmYXggU2Vj
+dXJlIGVCdXNpbmVzcyBDQS0yB4XDTk5MDYyMzEyMTQ0NcmVlDTE5MDYyMzEyMTQ0
+NVowTjELMAkGA1UEBEF1dGhvcml0eTEhMB8GCSqGSIb3
+DQEJ
+ARYSc32WoujDWojg4BrzzmH9CETMwZMJaLtVRKXxaeAufqDwSCg+i8VDXyh
+YGt+eSz6Bg86rvYbb7HS/y8oUl+DfUvEerf4Zh+AVPy3wo5ZShRXRtGak75BkQO7
+FYCTXOvnzAhsPz6zSvz/S2wj1VCCJkQZjiPDceoZJEcEnnW/yKYAHKswsuX
+n2FogNVBAcTCldhc2bnRylcnQgSobK/o5wXTXXtgZZKJYSi034DNHD6zt96rbHuSLBlxg
+J8pFUs4W7z8GZOeUaHxgMxURaa+dYo2jA1Rrpr7l7gUYYAS/QoD90KioHgE796Nc
+r6Pc5iaAIzy4RHT3Cq5Ji2F4zCS/iIqnDupzGUH9TQPwiNHleI2lKk/2lw0Xd8rYBRo
+kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmakQYJKowZheC5L2DMiJ+hekYJuFtwbIqVzdGVyMRAwDgYDVQQHDAdMIHBYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
+ODIyMTY0MTUxWjPDA6j
+IBBPM5i0AMBAf8wGQYJKUEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1akjOP
+MjYDVR0jBBgwFxMxeMyi/rE5OTggwMTgw
+ODIyMTY0MTUxW1nDFGwaQgVu/FsRkGIBKpwMTQxyvw+
+p5seTEfBsaWFiLEnZZWwMTgw
+ODIyMSJ7+hSHy6AX3dLm/WpcAeYTAOjkMO1MTg
+BAMTJEVudHJ1c3QgUm9vdCBDZXJ0ayGgq3oTDEwRDUkwxMBoGA1U
+82UJW/bsGe68SQsoWou7dC4A8HOd/7npCy
+0cE+U58DRLB+SRv5Hwf5+Kx5Lia78O5MBt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1
+E4qUoSek1nDFbS1yX2doNLGCEnZZp
+um0/QL3MUmV+GRMOrN
+-----END CERTIFICATE-----
+----BEGIN CERTIFICAwYDV----
+MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQgZ2V0IHlvdXIgb3duIneIvcnRpZmljYXRlIGZvcp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM
+HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw
+DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC
+nv+j6YDAgMBAAGjZjBkMBEGCWCGAqoCyLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji
+nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX
+rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWA1UEtFFZOrMLFPQS32eg9K0yZF6xRnIn
+jBJ7xUS0r+XWzMNqizF7eI+og7gwDQYJKoZI
+hvcNAQEFBQADggEBAKPYjtay284F5zLNAdQSa7AwECEGFwy0mMX5hFKeewptlQW3wfACtCJ0zr7iZ
+YYCLqJVwgcoA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8Zy----
+MIICkDCCAfmg
+AwIBAgIBATANBgkqhkiG9w0BAQyX2doNLGCEnZZpum0/QL3MUmk+GRMOrN
+-----
+dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjeTFFMEOgXeMHAxM8wMTgw
+OD
+CAwEAvTBH1jLuG2
+ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3Qs
+RgxRzwI0aF9pHsk5ubEFkbEFkoPRhSxglDM2MDcxNjgh5mDoO3TymVO1qEYBBRpS9mVEBV--END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEdG9yaWRhZwIBAgIBATANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMCRVMx
+IjAgjotMiAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1
+dG9yAgIB
+CBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2
+MjYzRgxR
+DEmMCRpb24xDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMrwoNwtUs22e5LeWU
+J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjW6sRa6tW8UvxDO
+JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY
+wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o
+koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN
+qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E
+Srg+iNiC3ZH5oSn7yzcdOAGT9HZnuMNSjSAQQFA0JhU8wI1NQ0kdvekhktdmnLfe
+xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u
+7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU
+sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI
+sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5XHAdkWquJR+A
+iPqQtCGJTP
+cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q-
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9wHC65B0Q2Sk0tjjKewPMurm9fkn
+5JVn1j+SgF7EChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj
+dXJlIGVCdXNpbmVzcyBDQS0yMB4XzTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0
+NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkVxdWlmYXggU2VjdXJlMSYwJAYD
+VQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCBnzANBgkqhkiG9w0B
+AQEFAAOpqoeJ5
+quGnM/b9Shkx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0G
+vxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
+BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
+I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFdhvvL5ndeCBTZWN1cmUxJjAkBgNVBAsTHULtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do
+lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc
+AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1gNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvk8fdLQ/uEH3Z/gfPqB63EHln+6eJNMR0T
+AQH/BAUwAwEB/zCBkQSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyyIWonX9t
+O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua
+AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP
+9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/
+eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
+0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END CAFHE4NvICMV----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
+dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTEDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4IKoFM8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0
+13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZgNVHREEIn2Fozy1MBJ3XJU8KDk2QixhWqJFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk
+U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i
+F6YM40AIOw7n6kIGFprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY
+oJ2daZH9zBdMQswCQYDVQQGEwJVUzEQ
+MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCbfgZJoz5iudXukEhxKe9XNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
+ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
+IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
+MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMVuY2SqGSIb3DQEBBQUA
+A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
+7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
+1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc
+MBoephojYn7qwVkDBF9qn1l
+uMrMTjANBgkqhkiG9w0BAQUFAAOCAXF1aWZheCBT
+ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDMu6nFL8eB8aHm8b
+N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
+KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
+kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
+CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
+Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
+imi5V5gEBAHlhYDAgMBAAGjZjBkMBEGCWCGSAGG+ERSWwauSCPc/L8my/uRan2Te
+2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhvcNDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
+/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
+F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2yiWAFAkcTisDxszGt
+TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dESW5jLjE3MDUGA1UEAxMuQU9M
+IFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNhE0T
+ZW7uXQvQ2xpZWGNrRniZ96LtKIVjNzGs7SNBbWVyaWNh
+IE9ubGluZSCB
+yzCahWqlwQ3JNgelmUxJjAkBgNVBAsTHUVxdWlmYXggU2Vj
+dXJlIcCk3RvKqsny
+rQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMcUesyBrJ6Zua
+AGAT/3BcnRpiSRuzFV
+QswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGMUUwQYQUqjPnFzxWMTMwn3qse0wJgSW5jLjEdMBsGA1UEAxMUR2Vvx5SBhsoNviyoynF7Y6yEb3+6+e0d
+-
+-----BE74KlfA1UECxMcW9y
+Z4cfrHuBnQfO3oKfN5ozNmr6mis=
+-----cVi/----END CERTInJM5IwDQYJ
+KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqrQ/307Kx+dOCQD32Oo0Hnp3DwQ1
+6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxTU
+oBEKIeZ1TatnaYzr4gNfTmeGl
+4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWmkUxT87z+gOn2sfvmWKZd7aVIl6K
+oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gsRkGIBKOQm0cYASSJGlVuX
+MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZyuzmKJAgIKXo1
+nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex
+t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz
+SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG
+BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+
+rCpSx4/VBEnkjWNHiDxpg+o0b70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/
+NIeWiu5T6CUVH6lmM0MkhbIyMTIFw0yOTEyMzEyMzU5NTlaMIGBVzZPrYPvH5JT
+oI53V9QYwbEjFNeeMA4GA1UwEYTBfoV2gWzBZMFcwVRYJBBQUZ2UpvpPm9Yy7zzAH
+BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wtkIvEcupdM5i3yDpvp8uSAyM
+I7P0Z24weRrTT3ZzbG9nby5naWo
+O3rHl+Ee5fSfwH/TZafC3ey78DAJ80M5+gKv
+MzEzatC7xrmYbvP33zGDLKe8bjq2RGlTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE
+p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y
+5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK
+WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3ldYB7MtEMze/aiCKm0o2etcxOXnGiYZ
+4fQRbxC1lfzChMMy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N
+hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WN
+hoEyIwOdyPdfwUpgpZKpsaSgYMN4h7Mi8yrrW6ntBas3D7Hi05V2Y1Z0jFdj2V7teJHqDKIjprS9esTR/h/xCA3JfgBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeNCIWonX9t
+O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua
+AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP
+9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/
+eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
+0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END +uPTqM1fp3DR----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
+dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE468OTP+cSuhVS5B1f5j8V/aBH4xBewRNzjMHPVKmIquNDM
+HO0oW369atyzkSTKQWI8/AIBvxwWMZQFl3Zuoq29YRdsTjCG8FE3KlDHqGKB3FtK
+qsGgtG7rL+VXxbErQHDbWk2hjh+9Ax/YA9SPTJlxvOKCzFjomDqG04Y48wApRXF1gSW5jLjEbMBkGA1UEAxMSR2VvVHJ1cIWMEsGnuVAVess+rLhDityq3RS6iYF+ATwj
+cSGIL4LcY/oCRaxFWdcqWERbt5+BO5JoPeI3JPV7bI92NZYJqFmduc4jq3TWg/0y
+cyfYaT5DdPauxYma51N86Xv2S/PBZYPejYqcPIiNOVn8qj8ijaHBZlCBckztImRP
+T8qAkbYpzBdMQswCQYDVQQGEwJVUzEQ
+MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheDsoKeLbnVqAc/EfMwvlF71XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
+MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
+ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAQphojYn7qwVkDBF9qn1l
+uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
+Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
+tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
+PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
+hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtXF1aWZheCBT
+ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDK3LpRFpxlmr8Y+1
+GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ
++mGuqPKIFJvCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd
+U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm
+NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY
+ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/
+ky2X7wxPuIrA2YDAgMBAAGjZjBkMBEGCWCGSAGG+j/ola09b5KROJ1WrIhVZPMq1
+CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq
+g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm
+fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c
+2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kDVR0Z6PDQpLv1U70qzlmwr25/
+bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRz----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIEN3DPtTANBNkqhkiECEAKtZn5ORf5eV288mBle3cQZZr1ME7a55lFEnSeEChMORXF1aWZheCBTZWN1cmUxJCCAQoCggEBALx+TAlVTE3DQEVUw
+DQYJKoZI2Vj
+dXJlIiEzFMpPVF/7LEyVMR0wGwYEXXgIvnsFUyBDMDYyMzEyMTQ0
+NVowTjELMAkGA1UEBhMCVVM0EHvI8wbEFkoPRhSxglDJXRmm/NzjdXJlMSYwJAYD
+VQQLEx1FcXVpZmF4CCAQoCgg1c2UsILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzLMAkG
+A1UEBhMCQkUxGTQUNFUyBDNviyoynF7Y6yEb3+6+e0dMKP/wXn2Z0GbdGhvcml0eTEhMB8GCSqGSIbJLJgJDhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII
+0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAete9VZbYQHZXATcXY+m3dM41CJVphI
+uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57H6lmM0ob3JpdHkgMAskEDaJLEChMDfgBl3X7hsuyw4jrg7Hl+n5kRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3
+YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc
+1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+ObzECAhzZXQvQ2xpZWU2GyYK7bcY6nlLMTM/QHCEorsTCOlMwiPH1d25Ryvr/mawND CERTIFICATE-----
+AkBgNVBAsTHUVxdWlmYXggU2Vj
+dXJlIGwwObV
+p9oa
+EzNNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy
+c2FBKOQ
+p74Klf9XXR9gj
+IBBPM5iQn9QwHQYDV4-BEGIN CERTIFICAInPRmJX1p7ijvMDNqtCYrscL9yuwNwXsvFcj4jjSm2jzVhKITGUIWVEX/r87yoqaKHee9570+sB3c4
+--
+cmsGO0/7GcA3WnR2gNVBAMTKkVudHTBniYU1r6mis=
+-----pT1UdEAQTMBGBDzI
+odz2uFHdh
+1voqZiegDfqnc1zqcPGUIWVEX/r87yoqaKHee9570+sB3c4
+-----E
+OzA5j
+IBBPM5ilNUQ019X2Ym76CGv2BTAeFw05
+OTA3MTIxNzMzoHuLCggEBNYW5
+50IEcnBhIChjKbEFkSwwKbKRY654eyN39/2n3qse0wJcAYJYDi1czF
+CpUCTPJ0b2qvzfvGnP4G3YQV94zpVvddtawJXa+ZH
+fAjIgrrep4c9oW24MFbCshmdZ8IAIVli
+zrQJIkRpivglWtvtDbc2fk7gu5Q+kCWHwmFHKdm9VLhjzCx9abQzNvQ3B5rB3UBU
+/OB4naCTuQk9I1F/RMIUdNsKvsvJMDRAmD7Q1yUgEiM9B0+c1lQn3y6ov8uQjI11
+S7zi6ESHzeZBCiVu6PQkAsVSD27smHIIDkjCCAnB3zCB3fGPjK50xRMECrWqeEmlvF/X++EFjcmFpIAQ+LW2WOgYMrrmzEjr7RQEHFwEDMCodpwPpY0nbJaHkb5BkHG2VydCgNVBAoTB0RljAyMva3G35MIIBIjAMDUxEwM0cQ+azfB/hHDDAmoCSgIo8yoMa+j49d/vYXJk
+IMIIDujCCAqKgAIBAgIGNh8VeEERM1x9Yjllpu9CtoAlEmEEExMzAyMDgyFBwEBBDYJMfcyCUGA1UEAxMewAaYTCCAq2gAwIBAgAQH/BAQMIIDujCCAqKgAwIBAgAQH/BA9YreQ0dXicDX5/Ob
+sRQ=
+---M4JzMl0tFgnBold+2DcIBcBlK0lRW
+HqzyRUyHuPU163hLBanInTsZIS5wNEqi9YngFXVF5yg3ADQnKeg3S/LvRJdrF1Ea
+w1adPBqK9kpGRjeM+sv1ZFo4aC4cAf8EzrhGBha/937ntag+RaypJXUie28/sJyU
+58dzq6wf7iWbwBbtt8pb8BzTcwfBwvNMm2+fG8oeqqg4MwlYsq78B+g23FW6L09A/nq9BqaBwZMifIYRCgDoxjaGENpdHkxJDAQE4Y1TR0/BvLB+WUF1ZAcYNlcy9pbmRleC5odG1sMEIGrUA
+A4IBDwAwggEKAoIBAnmHwUNzUwChMEVklTQTFzaWwxPTA7BgNVVmlCCAbcRd46B
+8cVJT11JENpdHrxsfKboNUgQXNzb2Nu/wNAQDAgEDAaLMAkGA1UE1oCggEgZUNUQ0YJYImRhsCqhkiGGO0/7GcInm2q2nb3V2DzMjg3MSjXf5WR0DIz
+NjEyWjBrYDVQQDEwRDUkwxMBoGA1ol4aR7OBKuEQLq4Gs
+J0/WwbgcQ3izDJr86iw8bmEbTUs9Z8FkZSBUZOmDAGJFtqkIk7mpM0sYmsL4h4hO
+291xNBrBVNpGP+DTKqttVCL1OmLIG+6
+KYnX3ZHu01yiMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswV95WHm6h
+2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E
+lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV
+ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+SKOeq
+299yOIzzlr3xF7zSujtFWsan9sYXiwGd/Bmor0WluDpI/k4+oKsGGelT84ATB+0t
+vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL
+dXe6YJ2E5/4tZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbYgEPAmEBqCQTcAARJl/6NVOIMPPyw/cDMezUb+B4wg4NfDtANS0c3oT
+CjhVAb6JVuo+o5SAGG+X/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/pY2FmPN3XEolWcR
+zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3
+LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd
+7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQzhABKMcPcw
+++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt
+398znM/jqGSI1I7mT1GvFpLgXPYHDdEwEB/wQFMAMBAf8wggFZBgNV
+HSAEggFQMIIBTDCCAUgGCisGAQQBsT4BAAAwgbXG4m2MQswCQYDCAx4BAwDhsIVjcuMTo0LjADAgYND CERTIFICATE-----
+DmIHRoaXc2UsIFZJU0EhIEluZm9ybWsTJlqttVCLuycPAtStNdKGWZKNBgkmk/f3
+u9RhsEFzc29jaWrO4x/nRNdefault CDEwlHU6Yxq7Th0D9AwpLQwDAwODNmr6I1ul
+CDkVudHJ1IhvcC321Y9YAwWjBhAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsBAQUd
+AfvDbbnvRG15RjF+Cv6pgsH/76tIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuLBgNi1EinbLDvhGMTCUdQ3ZHu01ygQQDAzN1cmUgZUJ1c2luZXNzIENBLTEwHhcNO
+6
+l2Qijvj/kBcLWqxEDwq2omYXkZAPy/mzdZDK9vZBv42pWUJGkzEXDK41Z0ohdX
+ZFwgBuHW73G3lVTMwWnQSaSxBNf0V2KJXLB1LRckaeNCYOTudNargFbYiCjh+20i
+/SN8RnNPflRzHqgsVVh1t0zzWkWlAhr62p3DRcMiXvOL8WAp0sdftAw6UYPvMPjtJotfy+pmjIlC++QU3o63tmsPm7IgbthknGziLgE3sucfFicv8GjLtI/C1AVj59o/g
+halMCXI5Etuz9c9OYmTaxhkVOmMd6RdVoUwiPDQyRvhlV7or7zaMavrZ2UT0qt2E
+1w0cslSsMoW0ZA3eQbuxNMYBhjJk1ZSBMaW1pdNCMEAIFoN27TyclhAO9J59SzS/
+ca3CBfYDdYDOqU8axCRM7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVJpNbYIxUFE6dDea/bow6be3ga8wAhpXYUVfmtJ3CPPPTVbMjMCqujmAuKBiPFyWHb
+mQdpNSYx/scuhMKZYdQN6X0uEyt8joW2hcdLzzW2LEc9zikv2G+fiRxkk78IvXbQ
+kIqUs38oWmu/vTMs7WXcFsziza6kPWKSBpUmv9+55CCmc2rBvveURNZNbyoLaxhN
+dBA2aGpawWqn3TYpjLgwi08hPwAuVDAHOrqK5MOeyti12HvOdUVmB/RtLdh6yumJ
+ivIj2C/LbgA2T/vwLwHMD8AiZfSr4k5hLQOCfZEWtTDVFN5ex5D8ofyrEK9yVHJn
+B+8phuiyJccg/ybdd+95RBTEvd07xQObdyPsoOy7Wjm1zK0Gc3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD
+VQQECAhs2hvcNAQEBBQeSXnIBBjAdBgNVHQ4EFgQULNVQQgzCahWqlwQ3JNgel
+BfBC6ZcdBtLc7LoTC1dda8tzIEZhcmdvYm5g0wDQYJKLEyNXYTAlcyBGYXJnbIjANGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLzaWwxPTA7ADJr8DExlHMgRmFyZ28/GekIXBwbqRJjox0r26kmqPZm9I4XJuiGMx1I4DQYJKoZVHQ8BAfY0MTA1UECxMKNhY2EAsTBBAUAMEUFNZXVi/CPNmFbSvtr2ZnJM5IYtCHrnAlU66+tXifPVoYb+O7AWXX1uw
+16OFNMQkpw0P
+lZPvy5TYnh+dXIVx6quTx8itc2VrbqnzPmrC3p/
+-----END CEQsCAwEAATE-----
+-----BEGIN CETIFICATE-----
+MIIB+jCCAWMCAgGjMAbAEmJpNbYIxUFE6dDea/b0Dlyz4zyXG9rgkMbFjXZJ/YVqDM7Jvk0/82bfuUER84A4n13
+5zHCLielTWi5MbqNQ1mXx3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHE
+SxP9cMIlrCL1dQu3U+SlK93OvRw6esP3E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4O
+JgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5OEL8pahbSCOz6+MlsoCu
+ltQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4jsNtlAHCE/CsLAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XvSbKYAydzG//apBGR7NjfRObTrdv
+GDeAU/7dTFJhaXogBEUwesIYBgtKEwdSb2t7hwcB
+CjQZjDAfToSJI6WjzwFCm/Aq2gAwIBAgIQAqLnXifPVoZ---BEGweRrTT6PUEAxwAQDA/30lC0b24g
+UHJhY3RpY2UgU3RhdGVNIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo
+7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrvm+0fazbuSCUlFLZWohDo7qd/
+0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0ROhPs7fpvcmR7
+nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx
+x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKCGD91QIroTmMatukgalHizqSQ
+33ZwmVxwQFS6GqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s81w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBg08wDgYDVR0PAQH/hjKSAxOTk5
+IEVudHJ1c3Qubmh1UEChMYVGhlIEdvIERhZCCAQoCggEBAMFtXifPVoYb+O7AWXIFXifPVoxMjE4NDYMRwEBxMIQQQLDB0P
+lZPY29yTYnh+dXICYW5rIE5BjAQBB0aGUMkGxC1P
+lZPv+xDmcjOPBy
+ZWZlcmVuY CERTIFICATE-----
+MIIB+jCCAWMCAgGjMA0GCSqGcxMjEO6lhTkgR0
+ot+3i9ypJU0BgNVBEgQ2hjEjAQBgNVBAgTCUJhcmNl
+b9uYTESMBAGA1UEBxMJQmFyY2Vsb25hMS
+4wLAYDVQQKEyVJUFMgSW50ZXJuZXQg
+cVibGlzaGluZyBTZXJ2aWNlcyBzLmwuMS
+swKQYDVQQKFCJpcHNAbWFpbC5pcHMu
+ZTIFICATE-----
+MIIB+jCCAWMCAgGjMA4/qfX1id9NEHif2P0tEs7c42TkfYyXG9rgkMbFjXZJ/Yub7S9eeKPCCGeOARBJe+r
+WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU
+Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0NRhbmT/DBMHAGTthP796EfvyXhdDcs
+HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj
+z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf
+SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl
+H6lmM0MkhgE0SqGSMJKoZI
+hvcND7/KCrto/8cI7pDQLEx1vlYpIFICwLqBkGCSG
+KIHPME4GA1UkMJY2GGtpTE-----
+MIIH9zCCB2CgA3dzcHJjYzIG9mIHOMAwGA1UECxMFRENTU0HmEBqCQTcAARJl/6NmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsNl
+LCB0
+BIGqMIGng0aW9uIEF1dGhvcml0eTEzMDEGA1UqGBi6SBixEjAQBgNVBAgTCUJhcmq81yx9uYTESMBAGA1UEBxMJQmFyY2Vsb25hMS4wLAYDVQQKEyVJUFMgSW50ZXJuZXoovdGhvcml0eTEeMBwGCSqGSIb3DQEJARYP
+aswKQYDVQQKFCJpcHNAbWFpbC5pcHAgGGMA0GCRJjox0r26kmqPZm9I4XJuiGMx1mdXRob3JpdHkgMTCCASIw
+DQYQoijl
+ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd
+/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+Nem
+AK3ymIGjifz6pB
+A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn
+k49yaXEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9
+iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv
+2G0xffX8oRAHh84vWdw+WNiG9w0BCQEWD2lwc0BtYWlsLmlwcy5lczAeFw0wMTEyMjkwMDUzNTha
+Fw0yNTMbXG4x5FYrnJmQ6AUJRs7Bjq1ZxN1ZfvdY+grEorsTCOlMwiPH1d25Ryvr/maD
+VQQKEw9HVEUgQ29ycGGV4LnBQwdpf5pXX/hvm4cmFtcHw5QwDQYJKoZ0cnVzdk
+w18DW9FvChMbWFJhbXAYMql1GNXRor5HA6CgAwIBAgIBXJuZXS0IS2Hod/7zvuRY
+UHRtbCjAQBgNVBAoTFumLhV/AyV0Jtu4S2I
+1DpAa5LxmZZk3tv/ePT
+BgNVHTcx
+NDA2hhaW5z1bR8SIb3UzBJbXwFgYD
+VQQKEw9HVEUgQ29ycGMDAyL3JlbmV3YWxDgNVBuaHRtbD8w
+NwYJYIZIAYb4QgEBCoWKGh0dHA6Ly93d3cuaXBzLmVzL2lwczIwn7pTKBBMFaWN5
+Q0FDLmh0bWwwbQYVR0fBGYwZDAuoCygKoYoaHR0cDovL3d3dy5p
+I4XJuiGMx1I4S+6+JNM3GOGvMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQsYJB69FbS6
+38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP
+KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q
+DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4
+qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9AqaifqsAxcZZPRa
+JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi
+PvryxS3T/dRlH6lmM0MkhH8wgZwYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSq
+GSIb3
+DQEBBQUAA4IBAQCFDFO5G9RaEIFoN27TyclhAO9MZPoj0GY4QJnM5i5ASs
+jVy16bYbK1FZg9pvlYpzaWwwK6cHBsaGJ9tTEv2dB8Xfjea4eH93d3YvsYyp9beP
+emV0IExvWEd25zIG9mIHVzZSwgdGhlIElcnRpZmljYFQYKKwYBBAGCNwIBFgYKKw4/qfBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR
+vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
+qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa
+IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy
+i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98mhXB50N9otg6tamN8jSZxNQQ4Qb9CYQQ
+O+7ETPTsJ3xCwnR8gooJybQDJbw81w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgXqkeAzgSW1wbGVtEO63vKEorsTCOlMwiPH1d25RyvrBerEfZ1jE
+O1hZibCQTDTIFICATE--zMzAWVFAy6xFhyI6+2Vp/G3AuIHogby5vL7fBw18DW9FvCxMbQC3nyHJ/J36FEwMTeWZpaAgEAmkg87z+gOGAYDVIcUkzBEMP9smcDQyByICh3vY
+sLSYxq7Thf2
+9w4LTJx1Jv
+mZyMBxODE3
+DKqC5kJNsggRGMM0GA1UdjB1tsIS7ywS
+zHb5BlmvXSHU0lq4oNTav3KaY1mSPd05u42veiWkXWmcSjK5yIS
+MIIwPh5r9FBS
+YmL9Yzt9fuzuOOpi9GyocY3h6YvJP8azZRCb92CRTG9jYS5jb20v
+QHYUxJZHMQhsxO/KvwJVEkLHNpdYbSBD
+F1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
+ARYSc34SRW9Q58g5DY1Hw7h
+gCRKBEdPdGn0MFHsfw7rlu/oQm7IChI/uWd9q5wwo77YojtTDjRnpgZsjqBeynX8T90vFILqsY2K
+5CF1OESalwvVr3sZiQX79lisuFKat92u6hBFikFIVxfHHB67Af+g7u0dEHdDW7lwy81MwFYxBTRy
+GA1UEBhMdHkgbECAhWki4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL
+se4CwSXrbLpX9EE3/QppEAE
+gfwwgfkwgfYGDSAy
+MQBvj0dETozEYMwgeQwgZofToSJI6WjzwFLaaLNGoGKKoYoaHA9SLRN0dB3
+eQJuPXdGWZK5IHpnb2RuaWUgeiBkb2thx5c CERtOiAiTuLCxXR5a22VwdCCsGAQUFBwY2ppIGRs
+Yvwn/xi8CAwiLjERMA8GocY3h6Y0IHd5rDyndPTCCnkMzCm6ZXo/GekIzBgNVHcIJAJlc4i+5jhpKq89HYUxJZHMQ
+KDuCDwAwoSJI6WjzwFCmjSlCgdbQzALFICATErhrcC6nZ5wbCEb
+MBken1UECAiBgNV5Z34xMdQWRkV5L3BjX35gFdPAsTK0eHQj9axY+IO6CizEqkzawJvFIw0C4aZOSGsfAOnjmhQb
+saZXJ0aWZpY2F0aW9uODHtVZd1T7TftXR/nEI1zR54njAbE+AAACCSiDkTEwggGRMIIBSQYRIHQB
+FIGh8Jpxt87AgSLwIEE3MDYGy769u3NtoaR0R3WNMdmt7fXTi0tyTQ9V4AIszxVjhnUPaKnF1KYy
+f8Tl+YTzk9ZfFkZ3kCdSaILZAOIrmqWNLPmjUQ5/JiMGho0e1YmWUcMci84+pIisTsyBBc/P32/W
++sz2H4FQAvOIMmxB7EJX9AdbnXn9EXZ+4nCqi0ft5z96ZqOJJiCB3vSaoYg+wdkcvb6souMJzuc2
+uptXtR1Xf3ihlHaGW+hmnpcwFA6AoNrom6Vgzk6U1ienx0Cw28BhRSKqzKkyXkuK8gRflZUx84uf
+tXncwKJrMiE3lvgOOBITRzcahirLer4c1w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBg9QEFA9cnVzdCBBQEPL/xoFEnpwvdr9G5Q1uCN0VWcu+2tsIS7ywS
+zHb5BlmvXSHU0lq4oNTzav3KaY1mSPd05u42veiWkXWmcSjK5yISMmmwPh5r9FBS
+YmL9Yzt9fuzuOOpi9GyocY3h6YvJP8a1zZRCb92CRSAwHWpI2jYMNxdHYUxJZHMQ
+KD/KvwQwVsfS---E2AQ8AMIIBCgBCQJwtS0MDI5NTNwYTAO
+NzrxUqFICAUzMDdaMHlVFJVU1RlZDEbMBlBMhcmNlbG9uYTKExZUUDf9Vs+kgxC6nZCTcC4g
+eiBvLm8uMSQT4fGWuWgfOgtm8KPicBbkN+8UEAx5gW6BZWNqjfzpZHMQ
+KDOBwDCBvTAdBgNVkNDYXRlUWlsLmlWNlcRA
+YtpbmVhIexkvAQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQsqgLJu
+QqY4yavbSgHg8CyfKTx4BokNSDOVz4eD9vptUr11Kqd06ED1hlH7Sg0goBFAfntNU/QTKwSBaNui
+me7C4sSEdgsKrPoAhGb4Mq8y7Ty7RqZz7mkzNMqzL2L2U4yQ2QjvpH8MH0IBqOWEcpSkpwnrCDIm
+RoTfd+YlZWKi2JceQixUUYIQ45Ox8+x8hHbvvZdgqtcvo8PW27qoHkp/7hMuJ44kDAGrmxffBXl/
+OBRZp0uO1CSLcMcVJzyr2phKhy406MYdWrtNPEluGs0GFDzd0nrIctiWAO4cmct4S72S9Q6e//0G
+O9f3/Ca5Kb2I1xYLj/xE+HgjHX9aD2M3IDzAfBgNggGMSqGSioaW5nIFNlcnZpY2VzIHMubC4xJWSXakFsKlnUWsi4SVqBaCB4pr17WXOzIHbMIHYMIHVBg0Hw+kqmP4/AhQKAGG+MIHDL2FjMzAy
+MzU5MDICMGkaZ1vhpaR
+-----XQgd3lYreQ30PsmeSB6Z29kbmllcSjK0ENBYyQ0xBUlbTogIl6Lfn60
+emrkDQtsSU=
+-----END CEBuK
+ym0cYYBBAGCNwIB
+FMJTEVXJ6ZWRvdDAYDGFzQFMAIi4wSfG2wLHqiMDn05DpKPbpVkQFqUSjYRDrgqZRCb92CLnBsvzMt
+TG6epb24mCapCocVoAQUFBwMHkvaXRlZC2EyUxIENBBjYTIudHh0MDmuASu7HwQIK1FwcmFpoDCGJbpVkQFqUSjYRDrgqmaWNhdGlv
+biBBd
+XRob3JpdHkxLjAsEERMTosvNYieEERM1XRlIGlzc3VlZCBieSGxGyl2CfpYHRonE82AVXO08kMINV
+HQ4EFgQUiGi/LtFBlILy4HNKVSzvHxBTM0HDowlAbE+AAACCSiDkTEwggGRMIIBSQYWTsCbqXrX
+hBBev5v5cIu-
+MIM8ww7oR0uMQRZoFSqvQUPWBYM2/TLI/f8UM9hSShUVj3zEsSj/vFHagUVmzuV
+Xo5u0WK8iaqATSyEVBhADHrPG6wYcLKJlagge/ILA0m+SieyP2sjYD9MUB9KZIEyBKv0429UuDTw
+6P7pslxMWJBSNyQxaLIs0SRKsqZZWkc7ZYAj2apSkBMX2Is1oHA+PwkF6jQMwCao/+CndXPUzfCF
+6caa9WwW31W26MlXCvSmJgfiTPwGvm4PkPmOnmWZ3CczzhHl4q7ztHFzshJH3sZWDnrWwBFjzz5e
+Pr3WHV1wA7EY6oT4zBx+2gT9XBTB1w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgUQEFAAAf8EBTADAEPq+qjANS0c3oT
+CjhVAb6JVuGDyOd6AzBhRckB4Y9wimvXSzIGluY29yzMzAuQ1ppQwMjI9IbqmFjTSWAlVT3Im50qWVuaXU---ENaXN0jY2VR29zcG9kYXJraZzAeFw0wMzAzQerEVJUFMgQ0EgQ0xBU0UzIENsRn1NU
+9jQ0Me
+MBDUwNTVr38oxMzA0MjgAwEB/zAMBMGgA0GCSqGSIb3DQEBBojPsCwJTGKbqz3Bzosq/SLmJbGqmODszFV0VRFlOHIilkfScmNlbG9uYTESMBAGA1UsFVjMJQmFyY2Vsb25hMS4wLAWgAwIBAgIBADFEw5OMdQWciB3cGlzdTog+SlIiR9U+jK7wYFuK13XneIviCfsuBH/0nLI/6
+l2QijvjLVdeOM62cPH2NERFxbS5FlUkfSv3fgesdVsTUFxZbGtE+/E0RMl
+KZQJHH3iwI7vRYubsi4EOLCjYsCOTFvgGRIpZzx7R7T5c0Di5XFkRU4gjBl7aHJoKb5SLzGlWdoX
+GsekVtl6keIFICizV2EafqjI8cnBWY7OxQ1ooLQp5AeFjXg+5PT0lO6TUZAubqjFbhVbxSWjqvdj
+93RGfyYE76MnNn4c2xWySD07n7uno06TC0IJe6+3WSX1h+76VsIFouWBXOoM7cxxiLjoqdBVu24+
+P8e81SukE7qEvOwDPmk9ZJFtt1nBNg8a1kaixcljrA/43XwOPz6qnJ+cIj/xywuIEF1dGhvwEB/spQgvepBGR7NjfRObTrdv
+GDeAU/7d0DYqjSjLd4H61/OCt3KfjDOgXeMdIAUEChMAQUFL2NhER64e
+ADADBagQBzLmVzMBoGA1UgADKAqjuAQt9fuhSHyXcGwwgY/Kr0VvIwSRCFNZX4AU2a7r85Cp1iJN
+W0Ca1LR6VG3996ShZaRjMGtGPPAUO6nIPcj2A7BMIGVCdXNpbmVKcyBDWmlD4xKzbnR/UDzVR0PB
+AMCAAW1pZW5pd94CTLVpc3RyYEjAQ3Nwb2wCY5tpYXJ0eSBhc3N1
+bWRwOi8vd3d3Lmlwcy5lcy9pggQ9/0sQAjqr2m7xPi71XAicY8wZkHq0zrY7nn1tFSdQ0PlJuYDAgMBAAGjZjBkMBEGCWCGSAGG+
+ldt/svO5c1MU08FKgrOXCGEbEPbQxhpM0xcd6Iv3dCo6qugEgjEs9Qm5CwUNKMnFsvR27cJWUvZb
+MVcvwlwCwclOdwF6u/QRS8bC2HYErhYo9bp9yuxxzuow2A94c5fPqfVrjXy+vDouchAm6+A5Wjzv
+J8wxVFDCs+9iGACmyUWr/JGXCYiQIbQkwlkRKHHlan9ymKf1NvIej/3EpeT8fKr6ywxGuhAfqofW
+pg3WJY/RCB4lTzD8vZGNwfMFGkWhJkypad3i9w3lGmDVpsHaWtCgGfd0H7tUtWPkP+t7EjIRCD9J
+HYnTR+wbbewc5vOI+UobR15ynGfFIaSIiMTVtm/81w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgexjaG2kdWN0c19zZwyah6FEnpwvdr9G5Q1uCN0VWcu+morKb
+adB4CDCCAUQdHRwUdIwSCATswggE3gBTrsxl588GlHKzcuh9morKbadBCKGCARqk
+ggEWMIIBEjE5Adg
+GA1UEBhMCRVMxEjAQBgNVBAgTCUJhcmNlbG9uYTESMBAGA1UE
+BxJQmFyY2VNBgk5hMS4wLAMDmgN6A1zTA2MDAExNGIN 3JsLm5MS4B0GA1yNTMB
+IENdXF1aWZheCBTZWN1cUEwjALBgNVHQ8EoQY
+MQlc3NlbjEOMAwGFNwLiB6IG8ubyQwIoAPMTk5
+5uZXQ03d3LmlgNVBABBAGCNwIB
+FQYKKwERTIFICADEuY29tL1NlY3VyQ0MNVBAgF+SfVm0cYT0NTUCATEQUyBDQSBVQQKEyVJUFMgSW50ZXJuZXQgcHVibGlzaGluZyo
+VCsaBStblXQYVNthe3dvaCrfvKpPXngh4almm988iIlEv9CVTaAdCfaJNihvA+Vs
+Qw8++ix1VqteMQE474/MV/YaXigP0Zr0QB+g+/7PWVlv+5U9GcnZsXx4DJay8AoI
+iB7Iy5Qf9iZiHm5BiPRIuUXT4ZRbZRYPh0/76vgRspTwd5Ed2qzBkxjaGY44QgEHLExVEAwIBRjAVBgeA4oDagNIdJQQ2IbLaFUufQb1nA5VJMEAQDAgEHwQDVR0wNqA0
+oDKGMIHPME4GA1UdIARHlmaWNhdGlv
+biBBdXRob3JpdHkxLjAsBgkqhyBDQBDTE
+
+6k6WuCB2e6bR64eVIHQMIHNHrPSBg4BAGA1UECBMoFICgwBNTTF9DBMCUGA1UEATkSmC5nIFGl5AGCNwoDAQYKKwYBBAZVqdPzWnRlcm5ldCBwdWJsaXNoaW5n
+IFSAi
+EFBQcDCAYKKwYBBAGCNwIB
+FQYKKwsLmlwcy5lcyBDLkkuRicmVMR4wY3Rzyb3cQwIoCUJhIuMEcXBzLmVzMBoGA1UdtgQTMBGB
+D2lwc0BtYWlsLmlwcy5lczBBBglgh
+Z2lhICAQ0ENBYyQ0xBU0UxIENAOCAgBfMV8wPf6s+x02kLQgH3thcoNyB
+S7RQZS
+C8uBzSlUs7x8QUzNBw6MJTBMQswCQYDVQQGEKEVrOY7cEHvsVgvoyZdytlbtgwEQQDEwRDXogQnIwpUeb+agRThHqtdB7Uq3EvZxz0ZWiQrRg5MV4YhP0HU2IsLInxhvt
+iUVmSFkIUsBCjzLoewOXA16d2oDyHhI/eE+VgAsp+2ANjZu4xRteHIHoYMsN218M
+eD2MLRsYS0U9xxAFK9gDj/KscPbrrdoqLvtPSMhUb4adJS9HLhvUe6BicvBf3A71
+iCNe431axGNDWKnpuj2KUpj4CFHYsWCXky847YtTXDjri9NIwJJauazsrSjK+oXp
+ngRS506mdQ7vWrtApkh8zhhWp7duCkjcCo1O8JxqYr2qEW1fXmgOISe010v2mmuv
+hHxPyVwoAU4KkOw0nbXZn53yak0is5+XmAjh0wWue44AssHrjC9nUh3mkLt6eAYDVQQIEwlCYXJjZWxvbmExEjAQBgNVBAcTCUJhcmNlbG9uYTEuMCwGA1UE
+ChMQEFA2TE2MTcxOTMP4vnLgQ0xBU0UzIENlcnRpZmljY
+SIb3DQEBAQUAA4I1UE
+ChQiaXBzQG1haWwuaXBzLmVzIEMuSS5GLiAgQi02MDkyO----
+MIICAxMWxvbm1zZRCb92CRTMIIK
+Yn4VLenZMdA1UdEAQTMBGBRMOTnVtpZ2ld3ENSTU6IDyiPqFb
+b3JnL2NAUAMxMzA
+ggEBApcHMDI4M9jcHMUQF
+M3tsIS7ywS
+zHb5BlmvXSHU0lqhkiG9w0BKaY1mSPd05u42veiWkXWmcSjK5yISMmmwPh5r9FBS
+YmL9Yzt9fuzuOOptx6quCRVMxEjAQBgNVBAgTCUJhHfD5YQUqjPnF+VHYUxJZHMQ
+KD/KvwPQ1NQ
+MIIBgNVhhlRO6mP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWM/9GwvARNuCVN+PqZmO
+4FqH8vTqhenUyqRkmAVT4YhLu0a9AXeLAYVDu+NTkYzsAUMAfu55rIKHNLlm6WbF
+KvLiKKz4p4pbUr+ToPcwl/TDotidloUdBAxDg0SL+PmQqACZDe3seJho2IYf2vDL
+/G4TLMbKmgTXTlWFuN0f4fJ0X40KPMbpggGgSqGSnPz+VtUFrWlymUWoCwSXB4ANKcI5YhR0lBAjNrN9GEwJCUjETMBkQ0pr17WXfzq/T220YoEKgQIY+AQEAwPMudwX/gNVBFPe+31haWcGwva3dK
+Ew9SLRvd2FuZSlczBBBglghkgBhEcpIcDovL2IIH6jC
+8VeEERM1gd0jBBglOiDf0xMIRTCBygYO6WXSIylvPwKCLAoljYXRgbcwb6ErPL
+v-END CgIwYBpelwcy5lcyBDLkkCB3eWYapekgemdvZG5pZSMDEwRva3Vt8KPiGW0g
+nZpY2VzIMubC4xKzApBgNVBAoUImlwE----WxvbmExLjAsR5IZIhvcynP
+sH/793
+IE9DU1AiLjRob3GA1UEAxMeQ09N7AQEAwPMudwX/hvmuZXMvaXBz
+MjAcmVwb3p5
+do+YviVtVBAMck8RLWCAeS9wY18sXVJcXzFfMCQYDVQQVHRMBAf8EBTADAQHe2PM
+GZB6tM62O559bRUnUND5SbkAsH3+
+gG9lbRgzl4jnCMvBALRQXtmDn9TyXQ/EKP+----END JwGDCMAXRpb24g
+UHJhY3RpY2UgU3RhdGVCXrKG5Def5lpRwmZom3UEDq
+bl7y4U3qomG4B+ok2FVZGgPZti+ZgvrenPj7PtbYCUBPsCSTNrznKinoT3gD9lQQ
+xkEHwdc6VD1GlFp+qI64u0+wS9Epatrdf7aBnizrOIB4LJd4E2TWQ6trspetjMIU
+upyWls1BmYUxB91R7QkTiAUSNZ87s3auhZuG4f0V0JLVCcg2rn7AN1rfMkgtNjAk
+GxiQbYWFljl6aatxR3odnnzVUe1I8uoY2JXpmmUcOG4dBsuaYziyKG3mtXCQWvug
+5qi9Mf3KUh1oSTKx6HfLjjNl1+wMB5Mdb8LF0XyZLdJM9yIZh7SBRsYm9QiXevYG/MIG8MA8GA1UdEwEB/wQFMAMBAf8wXQYIKwYBBQUHAQEEUTBPMCMG
+CCsGAkRXFBAIEludGVybmVL7eEYt5hhwjdrCA
+WXf82n+0S9atsIS7ywS
+zHb5BlmvXSHU0lq4oNTzav3KaY1mSPd05u42veiWkXWmcSjK5yISMmmwPh5r9FBS
+YmL9Yzt9fuzuOOpi9GyocY3h6YvJP8a1zZRCb92CRTzo3wno7wpVqVZHYUxJZHMQ
+KD/Kvwn/xi8CAwEAAaOCBIwNDE4MTQ1NDA1UECxMKYTrsxlBAf80MjR0cDaXBzMjAwMi9pcHMyMDAy
+Q0xBU0UxLwggE3gBTrsxl588GlHKzcuh9morKbadB4CKGCARqk
+ggEWMIIBEjELMAkGA1UEBhMCRVMxEjAQBgNVBAgTCUJhFBQADgYEAK9Dr/drIyllq2tMJQmFyY2BuK
+Yn4VLenZMdMvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCIFICATE---7BrBlbN5ma
+M5eg0BOTqoZ+9NBDvU8Lm5rTdrMswFTCathzpVVLK/JD4K3+4oCZ9SRAspEXE4gvwb08ASY6w5s+
+HpRkeJw8YzMFR5kDZD5adgnCAy4vDfIXYZgppXPaTQ8wnfUZ7BZ7Zfa7QBemUIcJIzJBB0UqgtxW
+Ceol9IekpBRVmuuSA6QG0Jkm+pGDJ05yj2eQG8jTcBENM7sVA8rGRMyFA4skSZ+D0OG6FS2xC1i9
+JyN0ag1yII/LPx8HK5J4W9MaPRNjAEeaa2qI9EpchwrOxnyVbQfSedCG1VRJfAsE/9tT9CMUPZ3x
+W20QjQcSZJqVcmGW9gVsXKQOVLsCAwEAAaOCAbMwggGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P
+AQH/BAQDAgEGMIIBBAlDUC1gBIH8MIH5MIH2Bg0rBgEEAb4/AgEKAQEBMIHkMIGaBggb25hFBQcC
+AjCBjRqBikNlcnR5ZmlrYXQgd3lzdGF3aW9ueSB6Z29kbmllIHogZG9rdW1lbnRlbTogIlBvbGl0
+eWthIEJVEkxETAPBgWNqaSBkbGEgUm9vdENBIi4gQ2VydHlmaWthdCB3eXN0YXdpb255IHByemV6
+IFJvb3RDQSB3IGhpZXJhcmNoaWkgQ0MgU2lnbmV0LjBFIGRhIEluZm9yARY5aHR0cDovL3d3dy5z
+aWduZXQucGwvcmVwb3p5dG9yaXVtL2Rva3VtZW50eS9wY19yb290Y2EudHh0MEQBhMCQHwQ9MDsw
+OaA3oDWGM2h0dHA6Ly93d3cucwJCUjETMnBsL3JlcG96eXRvcml1bSEgZGEgSW5vcFJhaGNhLmNy
+bDAfBgNVHSMEGDAWgBTAm8UjDQLhpk5Iax8A6eOaFBuxrzAdYWRvcQ4EFgQUwGxGyl2CfpYHRonE
+82AVXO08kMIwDQYJKoZIhvcNAQluZmADggEBABp1TAUsa+BeVWg4cjowc8yTJ5XN3GvN96GObMkx
+UGY7U9kVrLI71xBgoNVyzXTiMNDBvjh7vdPWjpl5SDiRpnnKiOFXA43HvNWzUaOkTu1mxjJsZsan
+ot1Xt6j0ZDC+03FjLHdYMyM9kSWp6afb4980EPYZCcSzgM5TOGfJmNii5Tq468VFKrX+52Aou1G2
+2Ohu+EEOlOrG7ylKv1hHUJJCjwN0ZVEIn1nDbrU9FeGCz8J9ihVUvnENEBbBkU37PWqWuHitKQDV
+tcwTwJJdR8cmKq3NmkwAm9fPacidQLpaw0WkuGrS+fEDhu1Nhy9xELP6NA9GRTCNxm/dXlcwnmY=
+-BAQAEND CERTIFICATEBAQAwwBAQAwBEGIN4BggrBgEFBQcCARYsaMIIFGjCCBAKgAwIBAgIEPV0tNDANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJQTDEfMB0BhMCE
+ChMWVFAgSW50ZXJ0SW5gU3AuVQQIby5vLjEkMCIFpei5CxMbQ2VudHJ1bSBDZXJ0eWZpa2Fjamkg
+EwJCUjETMRswGLoYsaHDExJDQyBTCxM0SW5gLSBSZGEgQ0EwHhcNMDIwODE2MTY0OTU2WMA0GjYw
+OTIxMTU0MjE5WjB2yoDCgLoYsaHR0cDovL2FjcmFpei5Y3BicmFzaWwuZ292LmJyL0xDUmFj
+cmF
+pei5jcmwwHQYDVR0OBBYEFIr68VeEERM1kEL6V0lUaQ2xPA3MA8GASAwHklDU/wQFMdMBAf8wDgY
+DVR0PAQQQ0EgS2xhc2EgMzCCASm+Uh2b/lQAcHVA
+isBmaLkWdkPADCCAQoCWdkwPLN3LanJtdue
+Ne6geWUTFENa+lEuzqELcoqhYB+a/tJcPEkc6TX/bYPzalRRjqs+quMP6KZTU0DixOrV+K7iWaqA
+iQ913HX5IBLmKDCrTVW/ZvSDpiBKbxlHfSNuJxAuVT6HdbzK7yAW38ssX+yS2tZYHZ5FhZcfqzPE
+OpO94mAKcBUhk6T/ki0evXX/ZvvktwmF3hKattzwtM4JMLurAEl8SInyEYULw5JdlfcBez2Tg6Db
+w34hA1A+ckTwhxzecrB8TUe2BnQKOs9vr2cCACpFFcOmPkM0Drtjctr1QHm1tYSqRFRf9VcV5tfC
+3P8QqoK4ONjtLPHc9x5NE1uK/FMAQUFADCBtDELMAkGA1UEBhMCQlIx
+EzARBgNVBAoTCklDUC1CcmFzaWwxPTA7BgNVBAsTNEluc3RpdHV0byBOYWNpb25h
+bCBkZSBUZWNCb2xvZ2lhIGRhIEluZm9ybWFjYW8gLSBJVEkxETAPBgNVBAcTCEJy
+YXNpbGlhMQswCQYDVQQIEwJERjExMC8GA1UEAxMoQXV0b3JpZGFkZSBDZXJ0aWZp
+Y2Fkb3JhIFJhaXogQnJhc2lsZWlyYTAeFw0wMTExMzAxMjU4MDBaFw0xMTExMzAy
+MzU5MDBaMIG0MQswCQYDVQQGEwJCUjETMBEGA1UEChMKSUNQLUJyYXNpbDE9MDsG
+A1UECxM0SW5zdGl0dXRvIE5hY2lvbmFsIGRlIFRlY25vbG9naWEgZGEgSW5mb3Jt
+YWNhbyAtIElUSTERMA8GA1UEBxMIQnJhc2lsaWExCzAJBgNVBAgTAkRGMTEwLwYD
+VQQDEyhBdXRvcmlkYWRlIENlcnRpZmljYWRvcmEgUmFpeiBCcmFzaWxlaXJhMIIB
+IjANBgkqhkiG9w0BAQEFAAOXvthcPHlH5BgGhlM
+ErJNXWlhlgA+Uh2b/lQAcHVA
+isamaLkWdkwPCIce95Mvn710KCAISA0CuHD4aznTU6pLoCDShW4
+7OR+GTpJUm1coTcUqlBHV9mra4VFrBcBuOkHZoBLq/jmE0QJWnpSEULDcH9J3mF0nqO9SM+mWyJG
+dsJF/XU/7smummgjMNQXwzQTtWORF+6v5KUbWX85anO2wR+M6YTBWC55zWpWi4RG3vkHFs5Ze2oF
+JTlpuxw9ZgxTnWlwI9QR2MvEhYIUMKMOWxw1nt0kKj+5TCNQQGh/VJJ1dsiroGh/io1DOcePEhKz
+1Ag52y6Wf0nJJB9yk0sFakqZH18F7eQecQImgZyyeRtsG95leNugB3BXWCW+KxwiBrtQTXv4dTEEwBAQAwOjA4BggrBgEFBQcCARYsaHR0
+cDovL2FjcmFpei5pY3BicmFzaWwEzYeIc7eyL0RQQ2FjO6ocGT5wZGYwPQYDVR0f
+BDYwNDAyoDCgLoYsaHR0cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJyL0xDUmFj
+cmFpei5jcmwwHQYDVR0OBBYEFIr68VeEERM1kEL6V0lUaQ2kxPA3MA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCEwOTIw3DQEgIh6OA4IBAQAZA5c1
+U/hgIh6OcgAyoDCgLoYsaHR0cDovL2FjcmFpei5bJSm5uDpt7TirYh1Uxe3fQaGl
+YjJe+9zd+izPRbBqXPVQA34EXcwk4qpWuf1hHriWfdrx8AcqSqr6CuQFwSr71UdEwEB
+/wQFMAMBAf8wDgYQhnZx2xJH/BAQDAgEggEiMA0GCSqGSIb3DQEf
+BDAA4IBDwA2tlLKAoIBAQCrr2vydnNpELfGW3Ks
+ARiDhJvwDtUe4AbWev+OfMc3+vA29nX8ZmIwno3gmItjo5DbUCCRiCMq5c9epcGu+kg4a3BJChVX
+REl8gVh0ST15rr3RKrSc4VgsvQzl0ZUraeQLl8JoRT5PLsUj3qwF78jUCQVckiiLVcnGfZtFCm+D
+CJXliQBDMB9XFAUEiO/DtEBs0B7wJGx7lgJeJpQUcGiaOPjcJDYOk7rNAYmmD2gWeSlepufO8luU
+YG/YDxTC4mqhRqfa4MnVO5dqy+ICj2UvUpHbZDB0KfGRibgBYeQP1kuqgIzJN4UqknVAJb0aMBSP
+l+9k2fAUdchx1njlbdcbAgMBAAGjggFtgNVBaTAPYWRvcRNVBAoEBTADmFzagNVBAsTNEluc3Rpd
+HV0byBOYWNpb25h
+bCBkZSBUZWNAb2xvZ2lhIGRhIEluZm9yWFjYW8gLSBJVEkxETAPBgNVBAcTC
+EJy
+YXNpbGlhMQswCQYDVQQIEwJERjExMC8GA1UEAxMoQXV03JpZGFkZSBDZXJ0aWZp
+Y2Fkb3Jh
+IFJhaXogQnJhc2lsZWlyYTAeFw0wMTExMzAxMjU4MDBaFw0xTExMzAy
+MzU5MDBaMIG0MQswCQYD
+VQQGEwJCUjETMBEGA1UEChMKSUNQLUJyYXNpbDE9MDsG
+A1UCxM0SW5zdGl0dXRvIE5hY2lvbmFs
+IGRlIFRlY25vbG9naWEgZGEgSW5mb3Jt
+cmFpeidDgQWBiBCcmFzaWxlaXJhMIIB
+IjANBgkqhkf
+YWRvcmEgUmFpeiBCcmFzaWxlaXJhMIIB
+IjANBgkqhkOG9w0BA8kO/c
+gAMCAQY+Uh2b/lQAcHVA
+
+isamaLkWdkwPGnY5QmYqnnO9OqFOWZxxb25UHRnaRF6IV9aaGit5BZufZj2Tq3v8L3SgE34GOoI
+cdRMMG5JEpEU4mN/Ef3oY6Eo+7HfqaPHI4KFmbDSPiK5s+wmf+bQSm0Yq5/h4ZOdcAESlLQeLSt1
+CQk2JoKQJ6pyAf6xJBgWEIlm4RXE4J3324PUiOp83kW6MDvaa1xY976WyInr4rwoLgxVl11LZeKW
+ha0RJJxJgw/NyWpKG7LWCm1fglF8JH51vZNndGYq1iKtfnrIOvLZq6bzaCiZm1EurD8HE6P7pmAB
+KK6o3C2OXlNfNIgwkDN/cDqk5TYsTkrpfriJPdxXBH8hQOkW89gEwBAQAwOjA4BggrBgEFBQcCARYsaHR0
+cDovL2FjcmFpei5pY3BicmFzaWwD/TCCA2ayL0RQQ2Fjc4/gkowYDVR0jBIGbMIGY
+gBQWt1yoDCgLoYsaHR0cDovL2FjcmFpei5pY3BicmFzaWwuZ292LmJyL0xDUmFj
+cmFpei5jcmwwHQYDVR0OBBYEFIr68VeEERM1kEL6V0lUaQ2kxPA3MA8GA18wHwEB
+/wQFMZMBAf8wDgYDVR0PAQ
+MzULbGFzYSAxMB4XDTAzMTAxNzEyMjkwMloX
+DTExMDkyMzExMTgxN1owdjELMAkFpei5BhMCUEwxHhkiG9w0BAoTFlRQIEludGVyUjETIFNwLiB6
+IG8uby4xJDAi1gM7/3sTG0NxMC8ydW0hc2lsZWlyYTAeY2ppIFNpZ25ldDEgMB4Fpei5AxMXVQQGkxPA3MA8GIC0gVFNBIEtsYXNhIDEwgZ8ue2JNLd00UOSMMaiK/
+t79Y0AMIGJAoGBAOJYrISEtSsd
+uHajROh5/n7NGrkpYTT9NEaPe9+ucuQ37KxIbfJwXJjgUc1dw4wCkcQ12FJarD1X6mSQ4cfN/60v
+LfKI5ZD4nhJTMKlAj1pX9ScQ/MuyvKStCbn5WTkjPhjRAM0tdwXSnzuTEunfw0Oup559y3Iqxg1c
+ExflB6cfcofN8CCmHBGXgNVBkzBBlQ+TyG8EOjA4MDagNKAyhjBodHRwOi8vd3d3LnpI5idBVC5w
+bC9yZXBvenl0b3JpdW0vY3JsL2PgTDZCMS5jcmwTCklDUC1CmFzaWwxPTAeAMBYBhMCQJQx
+EzAM
+MAoGCCsG
+BDYBwMIMIHaYWRvcmAEgdIwgc8wgcwGDSGA1UQBvj8CZAoRAgEwgbowbwYIKwYBBQUH
+AgIwYxphc2lsZWlyYTAeFw0wMTExMzAxMjU4MDpnb2RuaWUgeiBkb2t1bWVoNSbtICJQb2xpdHlr
+Y8VeEERM1kEL6V0lUaQ2VQQGEwJCUjETIl+TWm5ha293YW5pZSBjemFzZW0iLjBHIGRhIEluZm9ybWRY7G7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
+GCSNe9FIGRlIFRlY25vbG9naWE0c2ExXzJf
+MS5wZGYwHwlDUC1jBBgwFoAUw4Me1Vl3VPtN+1dH+cQjXNHnieMHm9WkBR0OBBYEFJdDwEqtcavO
+Yd9u9tej53vWXwNBLj6+YxWdEwQCMAqfrU0j36NK2B5jc
+G8Y0f3/YEAnpiQkqLCJQYXUrqMHUEz
++z3rOqS0XzSFnVVLhkVssvXc8S3FkJIiQTUrkScjI4CToCzujj3EyfNxH6yiLlMbskF8I31JxIeB
+vueqV+s+o76CZm3ycu9hb0I4lswuxoT+q5ZzPR8Irrb51rZXlolR+7KtwMg4sFDJZ8RNgOf7tbAUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
+ggHKMB0GA1UdDgQWBBQWtTIb1MfFbGFz36yL0RQQ2FBAi5wZGYwPQYDVR0f
+BQFADCBv/CsLj6+YxWl50PpVVMx
+EDmW
+omTBAgTB0luZGlhbmExFTATkq9ve7cTDEiQKtwOIjFwL2NhczEoMC7
+6QI5pY3BfU29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJMC8GcmVzdDETMBEwwHQYDVRKaG97iwi1hc3Rlcj
+bgtxA+qvFTia1N2lsZGlyYTNhdGlvbiBBdXRoRS0LKHkxJTAjZGYw
+PQYDVR0fCQEWFmhvc3RtYXNuZ29Ac3BpLWluYy5vcmcEGMA0GCMwMTE1MTYyOTE3
+Bwb3JDc---E0D CERTIFWj5OvfPi2RSLXNLrAWygF6UtOucekq9ve7O/e0iQKtwO
+Ij1CodqwqsF
+YMlIBdpTwd5Ed2qz8zw87YC8pjhKKSRflk7myV6VmMAZLldpGJ9V
+zZPrYPvH5JT
+oI53V93lYRE9IwCQTDz6o2CTBKOvNfYOo9PSmCnhQVsRqGP9Md24
+6FZV/dxssRu
+FFxtbUFm3xuTsdQAw+7Lzzw9IYCpX2NlN3gX6T0K/CFcUHUZyX7G
+rGXrtaZghNB
+0m6lG5kngOcLqKT1inA62+tC4T7V2q
+SNfVfdQqe1z6RgRQ5MPB6
+rdoiLR3RodtM22LMcfwfqb5OrJNl7fwmvskgF7yP6sdD2bOfDIXhg9852jhY8/kL
+VOFe1ELAL2OyN4RAxk0rliZQVgeTgqvgkOVIBbNwgnjN6mqtuWzFiPL+NXQExq40
+I3whM+4lEiwSHaV+MYxWanMdhc+kImT50LKfkxcdcofN8CCmHBEfgNVBGhkiG9w0
+BAQEFAAOB63oQR1/vda/G4F6P4xLiN4E0vowgesBhMCQIwSB4zCB4IAHB9r9JlSK
+hMPaybawa1EyvZspMQqhgcSkgcUFBz4xCzAJkq9ve7YTAlVTMRATCklDUQQIEwdJ
+bmRpYW5hMRUwEDCxRQQHEwxJVo39X2YwcG9saXMxKDAm1gM7/30WH1NvZnR3YXJl
+IGluIHRoZSBQdWJsaWMzaWwuZ29lc3QxEzARHshRpocjCHUZyX7GrGXrtaIxIDAe6Y5nCBAMTF0JVEkxpZmljYXRpb24gQXV0a2lvbmR5MSUwIw2b/lQAcHVA
+ikBFhZo
+b3N0bWFzNSbiQHNwaS1pbmMub3JnggEAMAwaSjrxC1uRBgNVBAoTCfQb1nA5V9FrWk9pEQ4Dvmw2Mm/Abn8c2y1nO3fgpAIslxvi9iNBZDhQtJ0VQZY6wgSfANyDOR4DW
+iexO/AlorB49KnkFS7TjCAoLOZhcg5FaNiKnlstMI5krQmau1Qnb/vGSNsE/UGms
+1ts+QYPUs0KmGEAFUri2XzLy+aQo9Kw74VBvqnxvaaMeY5yMcKNOieYEwBAQAwOjA4BggrBgEFBQcCARYsaHR0
+cDovL2FjcmFpei5pY3BicmFzaWwID292LfyAzIFJvb3JAOiOtsn4KhQoNybDA0BglghkgBhvBQUe1z68yoDCgLoY
+saHR0cDVUzEQMAxA+qvFCBMHSW5kaWFuYTEVMBM+YxWl5xMMGj2m2D3CkXMoQXVz
+MSgwJBvUujNKEx9Tb2Z0d2FyZSBpbiB0aGUgUHVibGlj3uYoNSbiZXN0MRMwEWcyF2MYLEwpofBUUNAgMBAAGMR4wHsTNE/wQFMVeEERMaWZpY2F0ZSuTsdQAw+7Lzzw9
+q
+b2VyZ0BkZWJpYW4ub3JnMB4XDTrGXrtaZghNB
+0m6lG5kngOcLqagA
+-gwNTEz
+
+1iazzQUAA4IBTiazG5xS3KoTLACsCERTDFICATE-----
+MIIESzCCAzOgAwIBAg
+e0iQKtwOIj1CodqwqsF
+YMlIBdpTwd5Ed2qz8zw87YC8pjhKKSRflk7myV6VmMAZ
+LldpGJ9VzZPrYPvH5JT
+oI53V93lYRE9IwCQTDz6o2CTBKOvNfYOo9PSmCnhQVse
+MBMuV
+dFTiaV/dxssRu
+FFxtbUUApbLdheFh0
+ZWlSxdnp25p0q0XYw/7G92ELyFDfBUUNAgMBAAGjgdswgdgwHQYDVR0MIICIj5wZGYwPQYDVR0f
+BEFAAOCAg8ABMSM
+CgKCAgEA3DbmR0LCxFF1KYdAw9iOIQbSGE7r7yC9kDyFEBOMKVuUY/b0LfEGQpG5
+GcRCaQi/izZF6igFM0lIoCdDkzWKQdh4s/Dvs24t3dHLfer0dSbTPpA67tfnLAS1
+fOH1fMVO73e9XKKTM5LOfYFIz2u1IiwIg/3T1c87Lf21SZBb9q1NE8re06adU1Fx
+Y0b4ShZcmO4tbZoWoXaQ4mBDmdaJ1mwuepiyCwMs43pPx93jzONKao15Uvr0wa8u
+jyoIyxspgpJyQ7zOiKmqp4pRQ1WFmjcDeJPI8L20QcgHQprLNZd6ioFl3h1UCAHx
+ZFy3FxpRvB7DWYd2GBaY7r/2Z4GLBjXFS21ZGcfSxki+bhQog0oQnBv1b7ypjvVp
+/rLBVcznFMn5WxRTUQfqzj3kTygfPGEJ1zPSbqdu1McTCW9rXRTunYkbpWry9vjQ
+co7qch8vNGopCsUK7BxAhRL3pqXTT63AhYxMfHMgzFMY8bJYTAH1v+pk1Vw5xc5s
+zFNaVrpBDyXfa1C2x4qgvQLCxTtVpbJkIoRRKFauMe5e+wsWTUYFkYBE7axt8Feo
++uthSKDLG7Mfjs3FIXcDhB78rKNDCGOM7fkn77SwXWfWT+3Qiz5dW8mRvZYChD3F
+TbxCP3T9PF2sXEg2XocxLxhsxGjuoYvJWdAY4wCAs1QnLpnwFVAfBgkqhkiG9g8w
+ggILv2vcB5G5l1YjqrQ0cdE41xU2g0dr1zdkQjuOjVKdqzCB855tiKxjBIHpMIHm
+gL60P0oPMAajbaTE5Z34AuITeHq3Y6GBwqSBvzlcnsV3Kdts0nIqPj6uhTTZD0k=tl0THrUO/e0iQKtwOIj1CodqwqsF
+YMlIBdpTwd5Ed2qz8zw87YC8pjhKKSRflk7m
+yV6VmMAZLldpGJ9VzZPrYPvH5JT
+oI53V93lYRE9IwCQTDz6o2CTBKOvNfYOo9PS
+UECBMGRnhbmNlMQ4wDAYDVQQHEwVQYXJpczEQMA4GA1UEChMHUE0v
+U0dETjEOMA
+7G92ELyFfBUUNAgMBAAGjgdswgdgwHQYDVR0OBkA6I62yfgqFCgwDDCxRd7TmFza
+BAUwAIx
+EZTXB
+lghkgBhvhCoBgNVFbLUAcCgLoYsR0SBAIwADAu-----BEGIN C
+AQ0EIRYmyV6VmMAZLldpGJ9VzZPrYPvH5JT
+oI53V93lYRE9IwAwAwIBAgIFORFFEJQQEIxYhYXNpbHMhc2ljYS5zcGktaW5jLm9yZBhcml1WfdmucGVtMDIGCWCGSAGG
++EIBAwQlFiNodFTEczovL2NhLndswgdgwHQYDVR0L2JVEkQt4wQ05nBlbTAhIMlsshREEGjAYgRAO9Tm7GxX0rmQIUBCqsU5u1WxoZ5lEB6VHwpZD0
+n
+EzAEL0RQBjAN
+ZGYwPQYDVR0f
+BDYwpZ2NhEAtM294LnqsgMrfjLp3nI/yUuCXp3ir1UJogxU6M8Y
+PCggHam7AwIvUjki+RfPrWeQswN/2BXja367m1YBrzXU2rnHZxeb1NUON7MgQS4M
+AcRb+WU+wmHo0vBqlXDDxm/VNaSsWXLhid+hoJ0kvSl56WEq2dMeyUakCHhBknIP
+qxR17QnwovBc78MKYiC3wihmrkwvLo9FYyaW8O4x5otVm6o6+YI5HYg84gd1GuEP
+sTC8cTLSOv76oYnzQyzWcsR5pxVIBcDYLXIC48s9Fmq6ybgREOJJhcyWR2AFJS7v
+dVkz9UcZFu/abF8HyKZQth3LZjQl/GaD68W2MEH4RkRiqMEMVObqTFoo5q7Gt/5/
+O5aoLu7HaD7dAD0prypjq1/uSSotxdz70cbT0ZdWUoa2lOvUYFG3/B6bzAKb1B+P
++UqPti4oOxfMxaYF49LTtcYDyeFIQpvLP+QX4P4NAZUJurgNceQJcHdC2E3hQqlg
+g9cXiUPS1N2nGLar1CQlh7XU4vwuImm9rWgs/3K1mKoGnOcqarihk3bOsPN/nOHg
+T7jYhkalMwIsJWE3KpLIrIF0aGOHM3a9BX9e1dUCbb2v/ypaqknsmHlHU5H2DjRa
+yaXG67Ljxay2oHA1u8hRadDytaIybrw/oDc5fHE2pgXfDBLkFqfF1sHVG8VwP+YE
+o2KKs+zZYPumUK5FQhxvWXtaM
+zPcPEAxSTtAWYeXlCmy/F8dyRlecmPVsYGN6DnYeIco73SsDrusjBJIQYJKoZIhvcN
+AQkBDYwNDAyoDCgLoYsaHR0cDERTEc
+MBoAwwCgYIKTRGV1dHNjolTkVGVsZWtvbSBBRzAwEB/zAyBgNxMWVC1UZWxlU2Vj
+IFRydXN0GFkZV93lYjEjMCz6o2CTAxMa+Mw+VgQ39FuCIvjfwbF3QMZH/BAQIENB
+IDIEGMA0OTkwNzA5M5c1
+UAwsYlEX2zpI/pdpjM1OhvdOCAQAwDwYDVR0TAQH/BE
+44pEMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY
+YLxuj7GoPB7ZHP
+OpJk5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg
+Crpa/JosPL3Dt8
+ldeCFP1YtlLmNybDA0BglghkgBhvhCAQgEJxYl
+aHR0cDovL3d3dC6M14IspFLEU
+ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
+QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
+rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
+NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
+QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
+txa2bkp/cofN8CCmQjBAGhp7CbYAY9CXuL6xw3kbuvVT1xfgiXotF2wKsyudMzAP6Y5nCpRMECDAGmFzaAgEFMTAxNzE0MjkyMlowgYUxCzAJgNVBAYTAkZSMQ8wDQYDVWk9pAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
+tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
+IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
+6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
+Cm26OWMohpLzGITY+9HPBVZkVw=EwBAQAwOjA4BggrBgEFBQcCARYsa
diff --git a/app/modules/billing/libraries/transaction_log.php b/app/modules/billing/libraries/transaction_log.php
new file mode 100644
index 00000000..e07199fb
--- /dev/null
+++ b/app/modules/billing/libraries/transaction_log.php
@@ -0,0 +1,62 @@
+CI =& get_instance();
+
+ // load user_agent library
+ $this->CI->load->library('user_agent');
+ }
+
+ /**
+ * Log Event
+ *
+ * @param int $order_id
+ * @param int $subscription_id
+ * @param string $event
+ * @param array $data
+ * @param string $file
+ * @param int $line
+ */
+ function log_event ($order_id = 0, $subscription_id = 0, $event = '', $data = array(), $file = '', $line = '') {
+ $ip = $this->CI->input->ip_address();
+ $browser = $this->CI->agent->agent_string();
+ $date = date('Y-m-d H:i:s');
+
+ $data = (is_array($data)) ? serialize($data) : $data;
+
+ $insert_fields = array(
+ 'order_id' => (empty($order_id)) ? '0' : $order_id,
+ 'subscription_id' => (empty($subscription_id)) ? '0' : $subscription_id,
+ 'log_date' => $date,
+ 'log_event' => $event,
+ 'log_data' => (empty($data)) ? '' : $data,
+ 'log_ip' => $ip,
+ 'log_browser' => $browser,
+ 'log_file' => $file,
+ 'log_line' => $line
+ );
+
+ $this->CI->db->insert('transaction_log', $insert_fields);
+
+ return $this->CI->db->insert_id();
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/charge_data_model.php b/app/modules/billing/models/charge_data_model.php
new file mode 100644
index 00000000..db5d8903
--- /dev/null
+++ b/app/modules/billing/models/charge_data_model.php
@@ -0,0 +1,77 @@
+ $order_id,
+ 'order_data_key' => $key,
+ 'order_data_value' => $value
+ );
+
+ $this->db->insert('order_data', $insert_data);
+ }
+
+ /**
+ * Get charge data
+ *
+ * Gets all the data for a charge
+ *
+ * @param int $charge The order ID
+ *
+ * @return mixed Array containg authorization details
+ */
+
+ function Get ($order_id)
+ {
+ $this->db->where('order_id', $order_id);
+ $query = $this->db->get('order_data');
+ if($query->num_rows() > 0) {
+ $return = array();
+ foreach ($query->result_array() as $row) {
+ $return[$row['order_data_key']] = $row['order_data_value'];
+ }
+ return $return;
+ } else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Delete Order Data
+ *
+ * @param int $order_id
+ *
+ * @return void
+ */
+ function Delete ($order_id) {
+ $this->db->delete('order_data', array('order_id' => $order_id));
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/charge_model.php b/app/modules/billing/models/charge_model.php
new file mode 100644
index 00000000..be819b76
--- /dev/null
+++ b/app/modules/billing/models/charge_model.php
@@ -0,0 +1,517 @@
+CI =& get_instance();
+
+ $this->CI->load->library('billing/transaction_log');
+ }
+
+ /**
+ * Create a new order.
+ *
+ * Creates a new order.
+ *
+ * @param int $gateway_id The gateway to process this charge with.
+ * @param float $amount The amount of the order
+ * @param array $credit_card The credit card information
+ * @param int $subscription_id The ID # of the recurring charge
+ * @param int $customer_id The customer ID to link this order to
+ * @param float $customer_ip The IP address of the purchasing customer
+ *
+ * @return int $order_id The new order id
+ */
+ function CreateNewOrder($gateway_id, $amount, $credit_card = array(), $subscription_id = 0, $customer_id = FALSE, $customer_ip = FALSE)
+ {
+ $timestamp = date('Y-m-d H:i:s');
+ $insert_data = array(
+ 'gateway_id' => $gateway_id,
+ 'subscription_id' => $subscription_id,
+ 'amount' => $amount,
+ 'timestamp' => $timestamp,
+ 'refunded' => '0',
+ 'refund_date' => '0000-00-00 00:00:00'
+ );
+
+ if (isset($credit_card['card_num'])) {
+ $insert_data['card_last_four'] = substr($credit_card['card_num'],-4,4);
+ }
+
+ if (isset($customer_ip) and !empty($customer_ip)) {
+ $insert_data['customer_ip_address'] = $customer_ip;
+ }
+
+ if (isset($customer_id)) {
+ $insert_data['customer_id'] = $customer_id;
+ }
+
+ $this->db->insert('orders', $insert_data);
+ $order_id = $this->db->insert_id();
+
+ $this->CI->transaction_log->log_event($order_id, FALSE, 'order_created', $insert_data, __FILE__, __LINE__);
+
+ return $order_id;
+ }
+
+ /**
+ * Mark Refunded
+ *
+ * Marks a charge as refunded now
+ *
+ * @param $charge_id The charge ID to mark refunded
+ *
+ * @return boolean TRUE upon success
+ */
+ function MarkRefunded ($charge_id) {
+ $update_data = array(
+ 'refunded' => '1',
+ 'refund_date' => date('Y-m-d H:i:s')
+ );
+
+ $this->db->update('orders', $update_data, array('order_id' => $charge_id));
+
+ $this->CI->transaction_log->log_event($charge_id, FALSE, 'order_refunded', FALSE, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Get Revenue by Day
+ *
+ * @param int $back_days (Optional) How many days to go back? Default: 30
+ * @return array Each day as key, total revenue as value
+ */
+ function GetRevenueByDay ($back_days = 30) {
+ $this->db->select('SUM(orders.amount) AS total_amount');
+ $this->db->select('DATE(orders.timestamp) AS day');
+ $this->db->where('orders.timestamp >',date('Y-m-d',time()-(60*60*24*$back_days)));
+ $this->db->group_by('DATE(orders.timestamp)');
+ $result = $this->db->get('orders');
+
+ $revenue = array();
+ foreach ($result->result_array() as $row) {
+ $revenue[] = array(
+ 'revenue' => $row['total_amount'],
+ 'day' => $row['day']
+ );
+ }
+
+ return $revenue;
+ }
+
+ /**
+ * Get total matching revenue
+ *
+ * Returns the total matching revenue with the relevant filters (same as GetCharges)
+ *
+ * @param int $params['gateway_id'] The gateway ID used for the order. Optional.
+ * @param date $params['start_date'] Only orders after or on this date will be returned. Optional.
+ * @param date $params['end_date'] Only orders before or on this date will be returned. Optional.
+ * @param int $params['customer_id'] The customer id associated with the order. Optional.
+ * @param string $params['customer_internal_id'] The customer's internal id associated with the order. Optional.
+ * @param int $params['id'] The charge ID. Optional.
+ * @param string $params['amount'] The amount of the charge. Optional.
+ * @param string $params['customer_last_name'] The last name of the customer. Optional.
+ * @param int $params['status'] Set to ok/failed to filter results. Optional.
+ * @param boolean $params['recurring_only'] Returns only orders that are part of a recurring subscription. Optional.
+ *
+ * @return string|bool Total amount
+ */
+
+ function GetTotalAmount($params)
+ {
+ // Check which search paramaters are set
+
+ if(isset($params['gateway_id'])) {
+ $this->db->where('gateway_id', $params['gateway_id']);
+ }
+
+ $this->load->library('field_validation');
+
+ if(isset($params['start_date'])) {
+ $valid_date = $this->field_validation->ValidateDate($params['start_date']);
+ if(!$valid_date) {
+ die($this->response->Error(5007));
+ }
+
+ $start_date = date('Y-m-d H:i:s', strtotime($params['start_date']));
+ $this->db->where('timestamp >=', $start_date);
+ }
+
+ if(isset($params['end_date'])) {
+ $valid_date = $this->field_validation->ValidateDate($params['start_date']);
+ if(!$valid_date) {
+ die($this->response->Error(5007));
+ }
+
+ $end_date = date('Y-m-d H:i:s', strtotime($params['end_date']));
+ $this->db->where('timestamp <=', $end_date);
+ }
+
+ if(isset($params['customer_id'])) {
+ $this->db->where('orders.customer_id', $params['customer_id']);
+ }
+
+ if(isset($params['amount'])) {
+ $this->db->where('orders.amount', $params['amount']);
+ }
+
+ if(isset($params['id'])) {
+ $this->db->where('orders.order_id', $params['id']);
+ }
+
+ if(isset($params['customer_id'])) {
+ $this->db->where('orders.customer_id', $params['customer_id']);
+ }
+
+ if(isset($params['customer_last_name'])) {
+ $this->db->like('customers.last_name',$params['customer_last_name']);
+ }
+
+ if(isset($params['customer_internal_id'])) {
+ $this->db->where('customers.internal_id', $params['customer_internal_id']);
+ }
+
+ if(isset($params['recurring_only']) && $params['recurring_only'] == 1) {
+ $this->db->where('orders.subscription_id <>', 0);
+ }
+
+ if (isset($params['recurring_id'])) {
+ $this->db->where('orders.subscription_id', $params['recurring_id']);
+ }
+
+ if (isset($params['status'])) {
+ if ($params['status'] == '1' or $params['status'] == 'ok') {
+ $this->db->where('orders.status','1');
+ $this->db->where('orders.refunded','0');
+ }
+ elseif ($params['status'] == '2' or $params['status'] == 'refunded') {
+ $this->db->where('orders.refunded','1');
+ }
+ else {
+ $this->db->where('orders.status','0');
+ }
+ }
+
+ $this->db->join('customers', 'customers.customer_id = orders.customer_id', 'left');
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+
+ $this->db->select_sum('amount','total_amount');
+ $query = $this->db->get('orders');
+
+ $array = $query->result_array();
+
+ $total = $array[0]['total_amount'];
+
+ return $total;
+ }
+
+ /**
+ * Search Orders.
+ *
+ * Returns an array of results based on submitted search criteria. All fields are optional.
+ *
+ * @param int $params['gateway_id'] The gateway ID used for the order. Optional.
+ * @param date $params['start_date'] Only orders after or on this date will be returned. Optional.
+ * @param date $params['end_date'] Only orders before or on this date will be returned. Optional.
+ * @param int $params['customer_id'] The customer id associated with the order. Optional.
+ * @param string $params['customer_internal_id'] The customer's internal id associated with the order. Optional.
+ * @param int $params['id'] The charge ID. Optional.
+ * @param string $params['amount'] The amount of the charge. Optional.
+ * @param string $params['customer_last_name'] The last name of the customer. Optional.
+ * @param int $params['status'] Set to ok/failed to filter results. Optional.
+ * @param boolean $params['recurring_only'] Returns only orders that are part of a recurring subscription. Optional.
+ * @param int $params['card_last_four'] Last 4 digits of credit card
+ * @param int $params['offset'] Offsets the database query.
+ * @param int $params['limit'] Limits the number of results returned. Optional.
+ * @param string $params['sort'] Variable used to sort the results. Possible values are date, customer_first_name, customer_last_name, amount. Optional
+ * @param string $params['sort_dir'] Used when a sort param is supplied. Possible values are asc and desc. Optional.
+ *
+ * @return array|bool Charge results or FALSE upon failure
+ */
+
+ function GetCharges($params)
+ {
+ // Check which search paramaters are set
+
+ if(isset($params['gateway_id'])) {
+ $this->db->where('gateway_id', $params['gateway_id']);
+ }
+
+ $this->load->library('field_validation');
+
+ if(isset($params['start_date'])) {
+ $valid_date = $this->field_validation->ValidateDate($params['start_date']);
+ if(!$valid_date) {
+ die($this->response->Error(5007));
+ }
+
+ $start_date = date('Y-m-d H:i:s', strtotime($params['start_date']));
+ $this->db->where('timestamp >=', $start_date);
+ }
+
+ if(isset($params['end_date'])) {
+ $valid_date = $this->field_validation->ValidateDate($params['start_date']);
+ if(!$valid_date) {
+ die($this->response->Error(5007));
+ }
+
+ $end_date = date('Y-m-d H:i:s', strtotime($params['end_date']));
+ $this->db->where('timestamp <=', $end_date);
+ }
+
+ if(isset($params['customer_id'])) {
+ $this->db->where('orders.customer_id', $params['customer_id']);
+ }
+
+ if(isset($params['amount'])) {
+ $this->db->where('orders.amount', $params['amount']);
+ }
+
+ if(isset($params['id'])) {
+ $this->db->where('orders.order_id', $params['id']);
+ }
+
+ if(isset($params['customer_id'])) {
+ $this->db->where('orders.customer_id', $params['customer_id']);
+ }
+
+ if(isset($params['customer_last_name'])) {
+ $this->db->like('customers.last_name',$params['customer_last_name']);
+ }
+
+ if(isset($params['customer_internal_id'])) {
+ $this->db->where('customers.internal_id', $params['customer_internal_id']);
+ }
+
+ if(isset($params['recurring_only']) && $params['recurring_only'] == 1) {
+ $this->db->where('orders.subscription_id <>', 0);
+ }
+
+ if (isset($params['recurring_id'])) {
+ $this->db->where('orders.subscription_id', $params['recurring_id']);
+ }
+
+ if (isset($params['card_last_four'])) {
+ $this->db->where('orders.card_last_four', $params['card_last_four']);
+ }
+
+ if (isset($params['status'])) {
+ if ($params['status'] == '1' or $params['status'] == 'ok') {
+ $this->db->where('orders.status','1');
+ $this->db->where('orders.refunded','0');
+ }
+ elseif ($params['status'] == '2' or $params['status'] == 'refunded') {
+ $this->db->where('orders.refunded','1');
+ }
+ else {
+ $this->db->where('orders.status','0');
+ }
+ }
+
+ if (isset($params['offset'])) {
+ $offset = $params['offset'];
+ }
+ else {
+ $offset = 0;
+ }
+
+ if(isset($params['limit'])) {
+ $this->db->limit($params['limit'], $offset);
+ }
+
+ if(isset($params['sort_dir']) and ($params['sort_dir'] == 'asc' or $params['sort_dir'] == 'desc' )) {
+ $sort_dir = $params['sort_dir'];
+ }
+ else {
+ $sort_dir = 'DESC';
+ }
+
+ $params['sort'] = isset($params['sort']) ? $params['sort'] : '';
+
+ switch($params['sort'])
+ {
+ case 'date':
+ $sort = 'timestamp';
+ break;
+ case 'customer_first_name':
+ $sort = 'first_name';
+ break;
+ case 'customer_last_name':
+ $sort = 'last_name';
+ break;
+ case 'amount':
+ $sort = 'amount';
+ break;
+ default:
+ $sort = 'timestamp';
+ break;
+ }
+
+ $sort_dir = isset($params['sort_dir']) ? $params['sort_dir'] : 'DESC';
+
+
+ $this->db->order_by($sort, $sort_dir);
+
+ $this->db->join('customers', 'customers.customer_id = orders.customer_id', 'left');
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+
+ $query = $this->db->get('orders');
+
+ $data = array();
+ if($query->num_rows() > 0) {
+ $i=0;
+ foreach($query->result() as $row) {
+ $data[$i]['id'] = $row->order_id;
+ $data[$i]['gateway_id'] = $row->gateway_id;
+ $data[$i]['date'] = local_time($row->timestamp);
+ $data[$i]['amount'] = money_format("%!^i",(float)$row->amount);
+ $data[$i]['card_last_four'] = $row->card_last_four;
+ $data[$i]['status'] = ($row->status == '1') ? 'ok' : 'failed';
+ $data[$i]['refunded'] = $row->refunded;
+ if ($row->refunded == '1') {
+ $data[$i]['refund_date'] = local_time($row->refund_date);
+ }
+
+ if($row->subscription_id != 0) {
+ $data[$i]['recurring_id'] = $row->subscription_id;
+ }
+
+ if($row->customer_id != 0) {
+ $data[$i]['customer']['id'] = $row->customer_id;
+ $data[$i]['customer']['internal_id'] = $row->internal_id;
+ $data[$i]['customer']['first_name'] = $row->first_name;
+ $data[$i]['customer']['last_name'] = $row->last_name;
+ $data[$i]['customer']['company'] = $row->company;
+ $data[$i]['customer']['address_1'] = $row->address_1;
+ $data[$i]['customer']['address_2'] = $row->address_2;
+ $data[$i]['customer']['city'] = $row->city;
+ $data[$i]['customer']['state'] = $row->state;
+ $data[$i]['customer']['country'] = $row->iso2;
+ $data[$i]['customer']['postal_code'] = $row->postal_code;
+ $data[$i]['customer']['email'] = $row->email;
+ $data[$i]['customer']['phone'] = $row->phone;
+ $data[$i]['customer']['date_created'] = local_time($row->date_created);
+ $data[$i]['customer']['status'] = ($row->active == 1) ? 'active' : 'deleted';
+ }
+
+ $i++;
+ }
+ } else {
+ return FALSE;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get Details of a specific order.
+ *
+ * Returns array of order details for a specific order_id.
+ *
+ * @param int $charge_id The order ID to search for.
+ *
+ * @return array|bool Array with charge info, FALSE upon failure.
+ */
+
+ function GetCharge($charge_id)
+ {
+ $params = array('id' => $charge_id);
+
+ $data = $this->GetCharges($params);
+
+ if (!empty($data)) {
+ return $data[0];
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Get Details of the last order for a customer.
+ *
+ * Returns array of order details for a specific order_id.
+ *
+ * @param int $customer_id The customer ID.
+ *
+ * @return array|bool Array with charge details or FALSE upon failure
+ */
+
+ function GetLatestCharge($customer_id)
+ {
+ $this->db->join('order_authorizations', 'order_authorizations.order_id = orders.order_id', 'inner');
+ $this->db->join('customers', 'customers.customer_id = orders.customer_id', 'left');
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+ $this->db->where('orders.customer_id', $customer_id);
+ $this->db->order_by('timestamp', 'DESC');
+ $this->db->limit(1);
+ $query = $this->db->get('orders');
+ if($query->num_rows() > 0) {
+ $row = $query->row();
+ return $this->GetCharge($row->order_id);
+ } else {
+ return FALSE;
+ }
+ }
+
+
+ /**
+ * Set the status of an order to either 1 or 0
+ *
+ * @param int $order_id The Order ID
+ * @param int $status The status ID. Default to 0.
+ *
+ * @return bool TRUE upon success.
+ */
+
+ function SetStatus($order_id, $status = 0)
+ {
+ $update_data['status'] = $status;
+ $this->db->where('order_id', $order_id);
+ $this->db->update('orders', $update_data);
+
+ $this->CI->transaction_log->log_event($order_id, FALSE, 'set_status', array('status' => $status), __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Get order authorization details
+ *
+ * Returns order authorization information.
+ *
+ * @param int $order_id The Order ID
+ *
+ * @return mixed Array containg authorization details
+ */
+
+ function GetChargeGatewayInfo($order_id)
+ {
+ $this->db->select('order_authorizations.*');
+ $this->db->where('order_authorizations.order_id', $order_id);
+ $this->db->join('order_authorizations', 'orders.order_id = order_authorizations.order_id', 'left');
+ $query = $this->db->get('orders');
+ if($query->num_rows() > 0) {
+ $array = $query->result_array();
+ return $array[0];
+ } else {
+ return FALSE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/customer_model.php b/app/modules/billing/models/customer_model.php
new file mode 100644
index 00000000..c3f0808f
--- /dev/null
+++ b/app/modules/billing/models/customer_model.php
@@ -0,0 +1,567 @@
+load->library('field_validation');
+ if (!empty($params['country'])) {
+ $country_id = $this->field_validation->ValidateCountry($params['country']);
+
+ if(!$country_id) {
+ die($this->response->Error(1007));
+ }
+ }
+ else {
+ $country_id = 0;
+ }
+
+ // If the country is US or Canada, we need to validate and supply the 2 letter abbreviation
+ $this->load->helper('states_helper');
+ $country_array = array(124,840);
+ if(in_array($country_id, $country_array)) {
+ $state = GetState($params['state']);
+ if($state) {
+ $params['state'] = $state;
+ } else {
+ die($this->response->Error(1012));
+ }
+ }
+
+ if ($customer_id = $this->SaveNewCustomer($params['first_name'], $params['last_name'], $params['company'], $params['internal_id'], $params['address_1'], $params['address_2'], $params['city'], $params['state'], $params['postal_code'], $country_id, $params['phone'], $params['email']))
+ {
+ return $customer_id;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Save the new customer
+ *
+ * Saves a new customer into the database and returns the resulting customer_id
+ *
+ * @param string $params['first_name'] Client's first name
+ * @param string $params['last_name'] Client's last name
+ * @param string $params['company'] Client's company. Optional.
+ * @param string $params['address_1'] Client's address line 1. Optional.
+ * @param string $params['address_2'] Client's address line 2. Optional.
+ * @param string $params['city'] Client's city. Optional.
+ * @param string $params['state'] Client's state. Optional. If the country is US or Canada, the 2-letter abbreviation should be used.
+ * @param string $params['postal_code'] Client's postal code. Optional.
+ * @param string $params['country'] Client's country in ISO format. Optional.
+ * @param string $params['phone'] Client's phone. Optional.
+ * @param string $params['email'] Client's email. Optional.
+ *
+ * @return int New Customer ID
+ */
+
+ // Save new customer
+ function SaveNewCustomer($first_name, $last_name, $company = '', $internal_id = '', $address_1 = '', $address_2 = '', $city = '', $state = '', $postal_code = '', $country_id = '', $phone = '', $email = '')
+ {
+ $insert_data = array(
+ 'first_name' => $first_name,
+ 'last_name' => $last_name,
+ 'company' => $company,
+ 'internal_id' => $internal_id,
+ 'address_1' => $address_1,
+ 'address_2' => $address_2,
+ 'city' => $city,
+ 'state' => $state,
+ 'postal_code' => $postal_code,
+ 'country' => $country_id,
+ 'phone' => $phone,
+ 'email' => $email,
+ 'active' => '1',
+ 'date_created' => date('Y-m-d, H:i:s'),
+ );
+ $this->db->insert('customers', $insert_data);
+
+ $customer_id = $this->db->insert_id();
+
+ return $customer_id;
+ }
+
+ /**
+ * Get the customer details.
+ *
+ * Returns a array containg all the customers's details. If the customer does not belong to the client, an error is returned.
+ *
+ * @param int $customer_id The customer ID
+ *
+ * @return mixed Array containing all the customer details.
+ */
+ function GetCustomerDetails($customer_id)
+ {
+ $this->db->where('customer_id', $customer_id);
+ $this->db->limit(1);
+ $query = $this->db->get('customers');
+ if($query->num_rows > 0) {
+ foreach($query->row() as $key => $value) {
+ $data[$key] = $value;
+ }
+ return $data;
+ } else {
+ die($this->response->Error(4000));
+ }
+ }
+
+
+ /**
+ * Updates a customer's details.
+ *
+ * Updates a customer details. If the customer does not belong to the client, an error is returned.
+ *
+ * @param int $customer_id The Customer to update.
+ * @param string $params['internal_id'] Customer's internal_id. Optional.
+ * @param string $params['first_name'] Customer's first name. Optional.
+ * @param string $params['last_name'] Customer's last name. Optional.
+ * @param string $params['company'] Customer's company. Optional.
+ * @param string $params['address_1'] Customer's address line 1. Optional.
+ * @param string $params['address_2'] Customer's address line 2. Optional.
+ * @param string $params['city'] Customer's city. Optional.
+ * @param string $params['state'] Customer's state. Optional.
+ * @param string $params['postal_code'] Customer's postal code. Optional.
+ * @param string $params['country'] Customer's country. Optional.
+ * @param string $params['phone'] Customer's phone. Optional.
+ * @param string $params['email'] Customer's email. Optional.
+ *
+ * @return mixed Array containing new customer_id
+ */
+ function UpdateCustomer($customer_id, $params)
+ {
+ $this->load->library('field_validation');
+
+ if(!isset($customer_id)) {
+ return FALSE;
+ }
+
+ if(isset($params['internal_id'])) {
+ $update_data['internal_id'] = $params['internal_id'];
+ }
+
+ if(isset($params['first_name'])) {
+ $update_data['first_name'] = $params['first_name'];
+ }
+
+ if(isset($params['last_name'])) {
+ $update_data['last_name'] = $params['last_name'];
+ }
+
+ if(isset($params['company'])) {
+ $update_data['company'] = $params['company'];
+ }
+
+ if(isset($params['internal_id'])) {
+ $update_data['internal_id'] = $params['internal_id'];
+ }
+
+ if(isset($params['address_1'])) {
+ $update_data['address_1'] = $params['address_1'];
+ }
+
+ if(isset($params['address_2'])) {
+ $update_data['address_2'] = $params['address_2'];
+ }
+
+ if(isset($params['city'])) {
+ $update_data['city'] = $params['city'];
+ }
+
+ if(isset($params['postal_code'])) {
+ $update_data['postal_code'] = $params['postal_code'];
+ }
+
+ if(isset($params['country'])) {
+ if ($params['country'] == '') {
+ $update_data['country'] = '';
+ }
+ else {
+ // Make sure the country is in the proper format
+ $country_id = $this->field_validation->ValidateCountry($params['country']);
+
+ if(!$country_id) {
+ die($this->response->Error(1007));
+ }
+ $update_data['country'] = $country_id;
+ }
+ }
+
+ if(isset($params['state']) and !empty($params['state'])) {
+ // If the country is US or Canada, we need to validate and supply the 2 letter abbreviation
+ $this->load->helper('states_helper');
+ $country_array = array(124,840);
+ if(isset($country_id) and in_array($country_id, $country_array)) {
+ $state = GetState($params['state']);
+ if($state) {
+ $update_data['state'] = $state;
+ } else {
+ die($this->response->Error(1012));
+ }
+ }
+ else {
+ $update_data['state'] = $params['state'];
+ }
+ }
+
+ if(isset($params['phone'])) {
+ $update_data['phone'] = $params['phone'];
+ }
+
+ if (isset($params['email'])) {
+ $update_data['email'] = $params['email'];
+ }
+
+ if(!isset($update_data)) {
+ die($this->response->Error(6003));
+ }
+
+ // Make sure they update their own customer
+ $this->db->where('customer_id', $customer_id);
+
+ if ($this->db->update('customers', $update_data)) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Delete a customer.
+ *
+ * Marks a customer as deleted. Does not actually delete the customer from the database, but only marks it as deleted.
+ *
+ * @param int $customer_id The customer ID
+ *
+ * @return bool TRUE upon success.
+ */
+ function DeleteCustomer($customer_id)
+ {
+ // Make sure they update their own customer
+ $this->db->where('customer_id', $customer_id);
+
+ $this->db->update('customers', array('active' => 0));
+
+ // cancel all active subscriptions
+ $this->db->where('customer_id', $customer_id);
+
+ $this->db->update('subscriptions', array('active' => 0));
+
+ return TRUE;
+ }
+
+ /**
+ * Get a list of customer details.
+ *
+ * Searches the database for customers belonging to the client and returns an array with all the details.
+ * All search parameters are optional.
+ *
+ * @param string $params['internal_id'] Customer's internal ID. Optional.
+ * @param string $params['first_name'] Customer's first name. Optional.
+ * @param string $params['last_name'] Customer's last name. Optional.
+ * @param string $params['company'] Customer's company. Optional.
+ * @param string $params['address_1'] Customer's address line 1. Optional.
+ * @param string $params['address_2'] Customer's address line 2. Optional.
+ * @param string $params['city'] Customer's city. Optional.
+ * @param string $params['state'] Customer's state. Optional.
+ * @param string $params['postal_code'] Customer's postal code. Optional.
+ * @param string $params['country'] Customer's country. Optional.
+ * @param string $params['phone'] Customer's phone. Optional.
+ * @param string $params['email'] Customer's email. Optional.
+ * @param int $params['plan_id'] Filter by active plan. Optional.
+ * @param int $params['deleted'] Set to 1 for deleted customers. Optional.
+ * @param string $pararms['sort'] Used to change the order of results returned. Possible values are date, first_name, and last_name. Optional.
+ * @param string $params['sort_dir'] Used only when a sort valus is padded. Possible values are asc and desc
+ *
+ * @return mixed Array containing the search results
+ */
+
+ function GetCustomers($params, $any_status = FALSE)
+ {
+ if(isset($params['deleted']) and $params['deleted'] == '1') {
+ $this->db->where('customers.active', '0');
+ }
+ elseif ($any_status == FALSE) {
+ $this->db->where('customers.active', '1');
+ }
+
+ // Check which search paramaters are set
+ if(isset($params['first_name'])) {
+ $this->db->where('first_name', $params['first_name']);
+ }
+
+ if(isset($params['internal_id'])) {
+ $this->db->where('internal_id', $params['internal_id']);
+ }
+
+ if(isset($params['id'])) {
+ $this->db->where('customers.customer_id', $params['id']);
+ }
+
+ if(isset($params['customer_id'])) {
+ $this->db->where('customers.customer_id', $params['customer_id']);
+ }
+
+ if(isset($params['last_name'])) {
+ $this->db->where('last_name', $params['last_name']);
+ }
+
+ if(isset($params['company'])) {
+ $this->db->where('company', $params['company']);
+ }
+
+ if(isset($params['address_1'])) {
+ $this->db->where('address_1', $params['address_1']);
+ }
+
+ if(isset($params['address_2'])) {
+ $this->db->where('address_2', $params['address_2']);
+ }
+
+ if(isset($params['city'])) {
+ $this->db->where('city', $params['city']);
+ }
+
+ if(isset($params['state'])) {
+ $this->db->where('state', $params['state']);
+ }
+
+ if(isset($params['postal_code'])) {
+ $this->db->where('postal_code', $params['postal_code']);
+ }
+
+ if(isset($params['country'])) {
+ $this->db->where('country', $params['country']);
+ }
+
+ if(isset($params['phone'])) {
+ $this->db->where('phone', $params['phone']);
+ }
+
+ if(isset($params['email'])) {
+ $this->db->where('email', $params['email']);
+ }
+
+ if (isset($params['offset'])) {
+ $offset = $params['offset'];
+ }
+ else {
+ $offset = 0;
+ }
+
+ if(isset($params['limit'])) {
+ $this->db->limit($params['limit'], $offset);
+ }
+
+ $params['sort'] = isset($params['sort']) ? $params['sort'] : '';
+
+ switch($params['sort'])
+ {
+ case 'date':
+ $sort = 'date_created';
+ break;
+ case 'first_name':
+ $sort = 'first_name';
+ break;
+ case 'last_name':
+ $sort = 'last_name';
+ break;
+ default:
+ $sort = 'last_name';
+ break;
+ }
+
+ $params['sort_dir'] = isset($params['sort_dir']) ? $params['sort_dir'] : '';
+
+ switch($params['sort_dir'])
+ {
+ case 'asc':
+ $sort_dir = 'ASC';
+ break;
+ case 'desc':
+ $sort_dir = 'DESC';
+ break;
+ default:
+ $sort_dir = 'DESC';
+ break;
+ }
+
+ $this->db->order_by($sort, $sort_dir);
+
+ if (isset($params['active_recurring']) or isset($params['plan_id'])) {
+ $this->db->join('subscriptions', 'customers.customer_id = subscriptions.customer_id', 'left');
+ }
+
+ if (isset($params['active_recurring'])) {
+ if($params['active_recurring'] == 1) {
+ $this->db->where('subscriptions.active', 1);
+ } elseif($params['active_recurring'] === 0) {
+ $this->db->where('subscriptions.active', 0);
+ }
+ }
+
+ if (isset($params['plan_id'])) {
+ $this->db->where('subscriptions.plan_id',$params['plan_id']);
+ $this->db->where('subscriptions.active','1');
+ }
+
+ $this->db->select('customers.*');
+ $this->db->select('countries.iso2');
+
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+ $this->db->group_by('customers.customer_id');
+ $query = $this->db->get('customers');
+
+ $data = array();
+ if($query->num_rows() > 0) {
+ $i=0;
+ foreach($query->result() as $row) {
+
+ $data[$i]['id'] = $row->customer_id;
+ $data[$i]['internal_id'] = $row->internal_id;
+ $data[$i]['first_name'] = $row->first_name;
+ $data[$i]['last_name'] = $row->last_name;
+ $data[$i]['company'] = $row->company;
+ $data[$i]['address_1'] = $row->address_1;
+ $data[$i]['address_2'] = $row->address_2;
+ $data[$i]['city'] = $row->city;
+ $data[$i]['state'] = $row->state;
+ $data[$i]['postal_code'] = $row->postal_code;
+ $data[$i]['country'] = $row->iso2;
+ $data[$i]['email'] = $row->email;
+ $data[$i]['phone'] = $row->phone;
+ $data[$i]['date_created'] = local_time($row->date_created);
+ $data[$i]['status'] = ($row->active == 1) ? 'active' : 'deleted';
+
+ $plans = $this->GetPlansByCustomer($row->customer_id);
+ if (!empty($plans)) {
+ $n=0;
+ foreach($plans as $plan) {
+ $data[$i]['plans'][$n]['id'] = $plan['id'];
+ $data[$i]['plans'][$n]['name'] = $plan['name'];
+ $data[$i]['plans'][$n]['amount'] = $plan['amount'];
+ $data[$i]['plans'][$n]['interval'] = $plan['interval'];
+ $data[$i]['plans'][$n]['notification_url'] = $plan['notification_url'];
+ $data[$i]['plans'][$n]['status'] = $plan['active'];
+ $n++;
+ }
+ }
+
+ $i++;
+ }
+ } else {
+ return FALSE;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get customer details.
+ *
+ * Searches the database for customers belonging to the client and with a specific customer_id.
+ *
+ * @param int $customer_id Customer ID.
+ *
+ * @return array|bool Customer data or FALSE upon failure.
+ */
+
+ function GetCustomer($customer_id)
+ {
+ $params = array('customer_id' => $customer_id);
+
+ $data = $this->GetCustomers($params, TRUE);
+
+ if (!empty($data)) {
+ return $data[0];
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Get plans by customer ID
+ *
+ * Gets all plans associated with the customer
+ *
+ * @param int $customer_id The ID of the Customer
+ *
+ * @return array All the plans, including status
+ */
+ function GetPlansByCustomer ($customer_id) {
+ $this->db->select('*');
+ $this->db->select('subscriptions.amount AS sub_amount');
+ $this->db->select('subscriptions.active AS sub_active');
+ $this->db->where('customer_id',$customer_id);
+ $this->db->join('plans','plans.plan_id = subscriptions.plan_id','INNER');
+ $result = $this->db->get('subscriptions');
+
+ $plans = array();
+
+ if ($result->num_rows() > 0) {
+ foreach ($result->result_array() as $row) {
+ $plans[] = array(
+ 'id' => $row['plan_id'],
+ 'name' => $row['name'],
+ 'amount' => $row['sub_amount'],
+ 'interval' => $row['interval'],
+ 'notification_url' => $row['notification_url'],
+ 'active' => ($row['sub_active'] == '1') ? 'active' : 'inactive'
+ );
+ }
+ }
+ else {
+ return FALSE;
+ }
+
+ return $plans;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/gateway_model.php b/app/modules/billing/models/gateway_model.php
new file mode 100644
index 00000000..d174bcb5
--- /dev/null
+++ b/app/modules/billing/models/gateway_model.php
@@ -0,0 +1,1331 @@
+CI =& get_instance();
+ $this->CI->load->library('billing/transaction_log');
+ }
+
+ /**
+ * Create a new gateway instance
+ *
+ * Creates a new gateway instance in the gateways table. Inserts the different gateway paramaters into the
+ * gateway_params table. These are not declared in this documentation as they can be anything. Returns the resulting gateway_id.
+ *
+ * @param string $params['gateway_type'] The type of gateway to be created (authnet, exact etc.)
+ * @param int $params['accept_mc'] Whether the gateway will accept Mastercard
+ * @param int $params['accept_visa'] Whether the gateway will accept Visa
+ * @param int $params['accept_amex'] Whether the gateway will accept American Express
+ * @param int $params['accept_discover'] Whether the gateway will accept Discover
+ * @param int $params['accept_dc'] Whether the gateway will accept Diner's Club
+ * @param int $params['enabled'] Whether the gateway is enabled or disabled
+ * @param string $params['alias'] The gateway's alias (optional)
+ *
+ * @return int New Gateway ID
+ */
+
+ function NewGateway($params)
+ {
+ // Get the gateway type
+ if(!isset($params['gateway_type'])) {
+ die($this->response->Error(1005));
+ }
+
+ $gateway_type = $params['gateway_type'];
+
+ // Validate the required fields
+ $this->load->library('billing/payment/'.$gateway_type);
+ $settings = $this->$gateway_type->Settings();
+ $required_fields = $settings['required_fields'];
+ $this->load->library('field_validation');
+ $validate = $this->field_validation->ValidateRequiredGatewayFields($required_fields, $params);
+
+ // Get the external API id
+ $external_api_id = $this->GetExternalApiId($gateway_type);
+
+ // Create the new Gateway
+
+ $create_date = date('Y-m-d');
+
+ $insert_data = array(
+ 'external_api_id' => $external_api_id,
+ 'alias' => (isset($params['alias']) and !empty($params['alias'])) ? $params['alias'] : $settings['name'],
+ 'enabled' => $params['enabled'],
+ 'create_date' => $create_date
+ );
+
+ $this->db->insert('gateways', $insert_data);
+
+ $new_gateway_id = $this->db->insert_id();
+
+ // Add the params, but not the client id or gateway type
+ unset($params['authentication']);
+ unset($params['gateway_type']);
+ unset($params['enabled']);
+ unset($params['type']);
+ unset($params['alias']);
+
+ $this->load->library('encrypt');
+
+ foreach($params as $key => $value)
+ {
+ $insert_data = array(
+ 'gateway_id' => $new_gateway_id,
+ 'field' => $key,
+ 'value' => $this->encrypt->encode($value)
+ );
+
+ $this->db->insert('gateway_params', $insert_data);
+ }
+
+ if ($this->config->item('default_gateway') == 0) {
+ $this->MakeDefaultGateway($new_gateway_id);
+ }
+
+ return $new_gateway_id;
+ }
+
+ /**
+ * Process a credit card charge
+ *
+ * Processes a credit card CHARGE transaction using the gateway_id to use the proper client gateway.
+ * Returns an array response from the appropriate payment library
+ *
+ * @param int $gateway_id The gateway ID to process this charge with
+ * @param float $amount The amount to charge (e.g., "50.00")
+ * @param array $credit_card The credit card information
+ * @param int $credit_card['card_num'] The credit card number
+ * @param int $credit_card['exp_month'] The credit card expiration month in 2 digit format (01 - 12)
+ * @param int $credit_card['exp_year'] The credit card expiration year (YYYY)
+ * @param string $credit_card['name'] The credit card cardholder name. Required only is customer ID is not supplied.
+ * @param int $credit_card['cvv'] The Card Verification Value. Optional
+ * @param int $customer_id The ID of the customer to link the charge to
+ * @param array $customer An array of customer data to create a new customer with, if no customer_id
+ * @param float $customer_ip The optional IP address of the customer
+ * @param string $return_url The URL for external payment processors to return the user to after payment
+ * @param string $cancel_url The URL to send if the user cancels an external payment
+ *
+ * @return mixed Array with response_code and response_text
+ */
+
+ function Charge($gateway_id, $amount, $credit_card = array(), $customer_id = FALSE, $customer = array(), $customer_ip = FALSE, $return_url = FALSE, $cancel_url = FALSE)
+ {
+ $this->CI->load->library('field_validation');
+
+ // Get the gateway info to load the proper library
+ $gateway = $this->GetGatewayDetails($gateway_id);
+
+ // is gateway enabled?
+ if (!$gateway or $gateway['enabled'] == '0') {
+ die($this->response->Error(5017));
+ }
+
+ // load the gateway
+ $gateway_name = $gateway['name'];
+ $this->load->library('billing/payment/'.$gateway_name);
+ $gateway_settings = $this->$gateway_name->Settings();
+
+ // validate function arguments
+ if ($amount == FALSE or ((empty($credit_card) and $gateway_settings['no_credit_card'] == FALSE and $gateway_settings['external'] == FALSE) and $amount != '0.00' and $amount != '0')) {
+ die($this->CI->response->Error(1004));
+ }
+
+ if (!empty($credit_card)) {
+ // validate the Credit Card number
+ $credit_card['card_num'] = trim(str_replace(array(' ','-'),'',$credit_card['card_num']));
+ $credit_card['card_type'] = $this->field_validation->ValidateCreditCard($credit_card['card_num'], $gateway);
+
+ if (!$credit_card['card_type']) {
+ die($this->response->Error(5008));
+ }
+
+ if (!isset($credit_card['exp_month']) or empty($credit_card['exp_month'])) {
+ die($this->response->Error(5008));
+ }
+
+ if (!isset($credit_card['exp_year']) or empty($credit_card['exp_year'])) {
+ die($this->response->Error(5008));
+ }
+ }
+
+ // Validate the amount
+ $amount = $this->field_validation->ValidateAmount($amount);
+
+ if($amount === FALSE) {
+ die($this->response->Error(5009));
+ }
+
+ // Get the customer details if a customer id was included
+ if (!empty($customer_id)) {
+ $this->CI->load->model('billing/customer_model');
+ $customer = $this->CI->customer_model->GetCustomer($customer_id);
+ $customer['customer_id'] = $customer['id'];
+ $created_customer = FALSE;
+ }
+ elseif (!empty($customer)) {
+ $this->CI->load->model('billing/customer_model');
+ // create customer record from attached information
+ // by Getting the customer after it's creation, we get a nice clean ISO2 code for the country
+ if (!isset($customer['first_name'])) {
+ $name = explode(' ', $credit_card['name']);
+ $customer['first_name'] = $name[0];
+ }
+ if (!isset($customer['last_name'])) {
+ $name = explode(' ', $credit_card['name']);
+ $customer['last_name'] = $name[count($name) - 1];
+ }
+
+ $customer_id = $this->CI->customer_model->NewCustomer($customer);
+ $customer = $this->CI->customer_model->GetCustomer($customer_id);
+ $customer['customer_id'] = $customer_id;
+ unset($customer_id);
+
+ $created_customer = TRUE;
+ }
+ else {
+ // no customer_id or customer information - is this a problem?
+ // we'll check if this gateway required customer information
+ if ($gateway_settings['requires_customer_information'] == 1) {
+ die($this->response->Error(5018));
+ }
+
+ $customer = array();
+ }
+
+ // if we have an IP, we'll populate this field
+ // note, if we get an error later: the first thing we check is to see if an IP is required
+ // by checking this *after* an error, we give the gateway a chance to be flexible and, if not,
+ // we give the end-user the most likely error response
+ if (!empty($customer_ip)) {
+ // place it in $customer array
+ $customer['ip_address'] = $customer_ip;
+ }
+ else {
+ $customer['ip_address'] = '';
+ }
+
+ // Create a new order
+ $this->CI->load->model('billing/charge_model');
+ $passed_customer = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $this->CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, 0, $passed_customer, $customer_ip);
+
+ // if amount is greater than $0, we require a gateway
+ if ($amount > 0) {
+ // make the charge
+ $response = $this->$gateway_name->Charge($order_id, $gateway, $customer, $amount, $credit_card, $return_url, $cancel_url);
+ }
+ else {
+ // it's a free charge of $0, it's ok
+ $response_array = array('charge_id' => $order_id);
+ $response = $this->CI->response->TransactionResponse(1, $response_array);
+ }
+
+ if (isset($created_customer) and $created_customer == TRUE and ($response['response_code'] != 1)) {
+ // the charge failed, so delete the customer we just created
+ $this->CI->customer_model->DeleteCustomer($customer['customer_id']);
+ }
+ elseif (isset($created_customer) and $created_customer == TRUE) {
+ // charge is OK and we created a new customer, we'll include it in the response
+ $response['customer_id'] = $customer['customer_id'];
+ }
+
+ // if it was successful, send an email
+ if ($response['response_code'] == 1) {
+ if (!isset($response['not_completed']) or $response['not_completed'] == FALSE) {
+ $this->CI->charge_model->SetStatus($order_id, 1);
+ }
+ else {
+ unset($response['not_completed']); // no need to show this to the end user
+ }
+ } else {
+ $this->CI->charge_model->SetStatus($order_id, 0);
+
+ // did we require an IP address?
+ if ($gateway_settings['requires_customer_ip'] == 1 and !$customer_ip) {
+ die($this->response->Error(5019));
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Create a new recurring subscription.
+ *
+ * Creates a new recurring subscription and processes a charge for today.
+ *
+ * @param int $gateway_id The gateway ID to process this charge with
+ * @param float $amount The amount to charge (e.g., "50.00")
+ * @param array $credit_card The credit card information
+ * @param int $credit_card['card_num'] The credit card number
+ * @param int $credit_card['exp_month'] The credit card expiration month in 2 digit format (01 - 12)
+ * @param int $credit_card['exp_year'] The credit card expiration year (YYYY)
+ * @param string $credit_card['name'] The credit card cardholder name. Required only is customer ID is not supplied.
+ * @param int $credit_card['cvv'] The Card Verification Value. Optional
+ * @param int $customer_id The ID of the customer to link the charge to
+ * @param array $customer An array of customer data to create a new customer with, if no customer_id
+ * @param float $customer_ip The optional IP address of the customer
+ * @param array $recur The details for a recurring charge
+ * @param int $recur['plan_id'] The ID of the plan to pull recurring details from (Optional)
+ * @param string $recur['start_date'] The start date of the subscription
+ * @param string $recur['end_date'] The end date of the subscription
+ * @param int $recur['free_trial'] The number of days to give a free trial before. Will combine with start_date if that is also set. (Optional)
+ * @param float $recur['amount'] The amount to charge every INTERVAL days. If not there, the main $amount will be used.
+ * @param int $recur['occurrences'] The total number of occurrences (Optional, if end_date doesn't exist).
+ * @param string $recur['notification_url'] The URL to send POST updates to for notices re: this subscription.
+ * @param string $return_url The URL for external payment processors to return the user to after payment
+ * @param string $cancel_url The URL to send if the user cancels an external payment
+ * @param int $renew The subscription that is being renewed, if there is one
+ * @param int $coupon_id A potential coupon_id
+ *
+ * @return mixed Array with response_code and response_text
+ */
+
+ function Recur($gateway_id, $amount = FALSE, $credit_card = array(), $customer_id = FALSE, $customer = array(), $customer_ip = FALSE, $recur = array(), $return_url = FALSE, $cancel_url = FALSE, $renew = FALSE, $coupon_id = 0)
+ {
+ $this->CI->load->library('field_validation');
+
+ // Get the gateway info to load the proper library
+ $gateway = $this->GetGatewayDetails($gateway_id);
+
+ if (!$gateway or $gateway['enabled'] == '0') {
+ die($this->response->Error(5017));
+ }
+
+ // load the gateway
+ $gateway_name = $gateway['name'];
+ $this->CI->load->library('billing/payment/'.$gateway_name);
+ $gateway_settings = $this->$gateway_name->Settings();
+
+ $amount = (float)$amount;
+
+ // validate function arguments
+ if (!empty($amount) and empty($credit_card) and $gateway_settings['external'] == FALSE and $gateway_settings['no_credit_card'] == FALSE) {
+ die($this->CI->response->Error(1004));
+ }
+
+ $this->load->library('field_validation');
+
+ if (!empty($credit_card)) {
+ // Validate the Credit Card number
+ $credit_card['card_num'] = trim(str_replace(array(' ','-'),'',$credit_card['card_num']));
+ $credit_card['card_type'] = $this->field_validation->ValidateCreditCard($credit_card['card_num'], $gateway);
+
+ if (!$credit_card['card_type']) {
+ die($this->response->Error(5008));
+ }
+
+ if (!isset($credit_card['exp_month']) or empty($credit_card['exp_month'])) {
+ die($this->response->Error(5008));
+ }
+
+ if (!isset($credit_card['exp_year']) or empty($credit_card['exp_year'])) {
+ die($this->response->Error(5008));
+ }
+ }
+
+ // are we linking this to another sub via renewal?
+ if (!empty($renew)) {
+ $this->CI->load->model('billing/recurring_model');
+ $renewed_subscription = $this->CI->recurring_model->GetSubscriptionDetails($renew);
+
+ if (!empty($renewed_subscription)) {
+ $mark_as_renewed = $renewed_subscription['subscription_id'];
+
+ /**
+ * this would automate the renewal subscription starting
+ * after the renewed subscription
+ *
+ if (strtotime($renewed_subscription['next_charge_date']) > time()) {
+ $recur['start_date'] = $renewed_subscription['next_charge_date'];
+ }
+ else {
+ $recur['start_date'] = date('Y-m-d');
+ }
+
+ $recur['free_trial'] = 0;*/
+ }
+ else {
+ $mark_as_renewed = FALSE;
+ }
+ }
+ else {
+ $mark_as_renewed = FALSE;
+ }
+
+ // Get the customer details if a customer id was included
+ $this->load->model('billing/customer_model');
+
+ if (!empty($customer_id)) {
+ $customer = $this->CI->customer_model->GetCustomer($customer_id);
+ $customer['customer_id'] = $customer['id'];
+ $created_customer = FALSE;
+ }
+ elseif (isset($customer) and !empty($customer)) {
+ // look for embedded customer information
+ // by Getting the customer after it's creation, we get a nice clean ISO2 code for the country
+ $customer_id = $this->CI->customer_model->NewCustomer($customer);
+ $customer = $this->CI->customer_model->GetCustomer($customer_id);
+ $customer['customer_id'] = $customer_id;
+ unset($customer_id);
+ $created_customer = TRUE;
+ }
+ else {
+ // no customer_id or customer information - is this a problem?
+ // we'll check if this gateway required customer information
+ if ($gateway_settings['requires_customer_information'] == 1) {
+ die($this->response->Error(5018));
+ }
+
+ // no customer information was passed but this gateway is OK with that, let's just get the customer first/last name
+ // from the credit card name for our records
+ if (!isset($credit_card['name'])) {
+ die($this->response->Error(5004));
+ } else {
+ $name = explode(' ', $credit_card['name']);
+ $customer['first_name'] = $name[0];
+ $customer['last_name'] = $name[count($name) - 1];
+ $customer['customer_id'] = $this->CI->customer_model->SaveNewCustomer($customer['first_name'], $customer['last_name']);
+ $created_customer = TRUE;
+ }
+ }
+
+ // if we have an IP, we'll populate this field
+ // note, if we get an error later: the first thing we check is to see if an IP is required
+ // by checking this *after* an error, we give the gateway a chance to be flexible and, if not,
+ // we give the end-user the most likely error response
+ if (!empty($customer_ip)) {
+ // place it in $customer array
+ $customer['ip_address'] = $customer_ip;
+ }
+ else {
+ $customer['ip_address'] = '';
+ }
+
+ if (isset($recur['plan_id'])) {
+ // we have a linked plan, let's load that information
+ $this->CI->load->model('billing/plan_model');
+ $plan_details = $this->CI->plan_model->GetPlanDetails($recur['plan_id']);
+
+ $interval = (isset($recur['interval'])) ? $recur['interval'] : $plan_details->interval;
+ $notification_url = (isset($recur['notification_url'])) ? $recur['notification_url'] : $plan_details->notification_url;
+ $free_trial = (isset($recur['free_trial'])) ? $recur['free_trial'] : $plan_details->free_trial;
+ $occurrences = (isset($recur['occurrences'])) ? $recur['occurrences'] : $plan_details->occurrences;
+
+ // calculate first charge amount:
+ // 1) First charge is main $amount if given
+ // 2) If no $recur['amount'], use plan amount
+ // 3) Else use $recur['amount']
+ if (isset($amount)) {
+ $amount = $amount;
+ }
+ elseif (isset($recur['amount'])) {
+ $amount = $recur['amount'];
+ }
+ elseif (isset($plan_details->amount)) {
+ $amount = $plan_details->amount;
+ }
+
+ $amount = $this->field_validation->ValidateAmount($amount);
+
+ if ($amount === FALSE) {
+ die($this->response->Error(5009));
+ }
+
+ // store plan ID
+ $plan_id = $plan_details->plan_id;
+ } else {
+ if (!isset($recur['interval']) or !is_numeric($recur['interval'])) {
+ die($this->response->Error(5011));
+ }
+ else {
+ $interval = $recur['interval'];
+ }
+
+ // Check for a notification URL
+ $notification_url = (isset($recur['notification_url'])) ? $recur['notification_url'] : '';
+
+ // Validate the amount
+ if ($this->field_validation->ValidateAmount($amount) === FALSE) {
+ die($this->response->Error(5009));
+ }
+
+ $plan_id = 0;
+ $free_trial = (isset($recur['free_trial']) and is_numeric($recur['free_trial'])) ? $recur['free_trial'] : FALSE;
+ }
+
+ // Validate the start date to make sure it is in the future
+ if (isset($recur['start_date'])) {
+ // adjust to server time
+ $recur['start_date'] = server_time($recur['start_date'], 'Y-m-d', true);
+
+ if (!$this->field_validation->ValidateDate($recur['start_date']) or $recur['start_date'] < date('Y-m-d')) {
+ die($this->response->Error(5001));
+ } else {
+ $start_date = date('Y-m-d', strtotime($recur['start_date']));
+ }
+ } else {
+ $start_date = date('Y-m-d');
+ }
+
+ // do we have to adjust the start_date for a free trial?
+ if ($free_trial) {
+ $start_date = date('Y-m-d', strtotime($start_date) + ($free_trial * 86400));
+ }
+
+ // get the next payment date
+ if (date('Y-m-d', strtotime($start_date)) == date('Y-m-d')) {
+ $next_charge_date = date('Y-m-d', strtotime($start_date) + ($interval * 86400));
+ }
+ else {
+ $next_charge_date = date('Y-m-d', strtotime($start_date));
+ }
+
+ // if an end date was passed, make sure it's valid
+ if (isset($recur['end_date'])) {
+ // adjust to server time
+ $recur['end_date'] = (!isset($end_date_set_by_server)) ? server_time($recur['end_date']) : $recur['end_date'];
+
+ if (strtotime($recur['end_date']) < time()) {
+ // end_date is in the past
+ die($this->response->Error(5002));
+ } elseif(strtotime($recur['end_date']) < strtotime($start_date)) {
+ // end_date is before start_date
+ die($this->response->Error(5003));
+ } else {
+ // date is good
+ $end_date = date('Y-m-d', strtotime($recur['end_date']));
+ }
+ } elseif (isset($occurrences) and !empty($occurrences)) {
+ // calculate end_date from # of occurrences as defined by plan
+ $end_date = date('Y-m-d', strtotime($start_date) + ($interval * 86400 * $occurrences));
+ } elseif (isset($recur['occurrences']) and !empty($recur['occurrences'])) {
+ // calculate end_date from # of occurrences from recur node
+ $end_date = date('Y-m-d', strtotime($start_date) + ($interval * 86400 * $recur['occurrences']));
+ } else {
+ // calculate the end_date based on the max end date setting
+ $end_date = date('Y-m-d', strtotime($start_date) + ($this->config->item('max_recurring_days_from_today') * 86400));
+ }
+
+ if (!empty($credit_card) and isset($credit_card['exp_year']) and !empty($credit_card['exp_year'])) {
+ // if the credit card expiration date is before the end date, we need to set the end date to one day before the expiration
+ $check_year = ($credit_card['exp_year'] > 2000) ? $credit_card['exp_year'] : '20' . $credit_card['exp_year'];
+ $expiry = mktime(0,0,0, $credit_card['exp_month'], days_in_month($credit_card['exp_month'], $credit_card['exp_year']), $check_year);
+
+ $date = strtotime($next_charge_date);
+ while ($date < strtotime($end_date)) {
+ if ($expiry < $date) {
+ $end_date = date('Y-m-d', $date);
+ break;
+ }
+
+ $date = $date + ($interval * 86400);
+ }
+ }
+
+ // adjust end date if it's less than next charge
+ if (strtotime($end_date) <= strtotime($next_charge_date)) {
+ // set end date to next charge
+ $end_date = $next_charge_date;
+ $total_occurrences = 1;
+ }
+
+ // figure the total number of occurrences
+ $total_occurrences = round((strtotime($end_date) - strtotime($start_date)) / ($interval * 86400), 0);
+ if ($total_occurrences < 1) {
+ // the CC expiry date is only going to allow 1 charge
+ $total_occurrences = 1;
+ }
+
+ // if they sent an $amount with their charge, this means that their first charge is different
+ // so now we need to grab the true recurring amount, unless they overrode it
+ if (isset($recur['amount'])) {
+ $recur['amount'] = $recur['amount'];
+ }
+ elseif (is_object($plan_details) and isset($plan_details->amount)) {
+ $recur['amount'] = $plan_details->amount;
+ }
+ else {
+ $recur['amount'] = $amount;
+ }
+
+ // Save the subscription info
+ $this->CI->load->model('billing/recurring_model');
+ $card_last_four = (isset($credit_card['card_num'])) ? substr($credit_card['card_num'],-4,4) : '0';
+ $subscription_id = $this->CI->recurring_model->SaveRecurring($gateway['gateway_id'], $customer['customer_id'], $interval, $start_date, $end_date, $next_charge_date, $total_occurrences, $notification_url, $recur['amount'], $plan_id, $card_last_four, $coupon_id);
+
+ // get subscription
+ $subscription = $this->CI->recurring_model->GetRecurring($subscription_id);
+ // is there a charge for today?
+ $charge_today = (date('Y-m-d', strtotime($subscription['date_created'])) == date('Y-m-d', strtotime($subscription['start_date']))) ? TRUE : FALSE;
+
+ // set last_charge as today, if today was a charge
+ if ($charge_today === TRUE) {
+ $this->CI->recurring_model->SetChargeDates($subscription_id, date('Y-m-d'), $next_charge_date);
+ }
+
+ // if amount is greater than 0, we require a gateway to process
+ if ($recur['amount'] > 0) {
+ // recurring charges are not free
+ $response = $this->CI->$gateway_name->Recur($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences, $return_url, $cancel_url);
+ }
+ elseif ($recur['amount'] <= 0 and $amount > 0) {
+ // recurring charges are free, but there is an initial charge
+
+ // can't be an external gateway
+ if ($gateway_settings['external'] == TRUE) {
+ die($this->response->Error(5024));
+ }
+
+ // must have a start date of today
+ if ($charge_today !== TRUE) {
+ die($this->response->Error(5025));
+ }
+
+ $this->CI->load->model('billing/charge_model');
+ $customer['customer_id'] = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $this->CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer_ip);
+ $response = $this->CI->$gateway_name->Charge($order_id, $gateway, $customer, $amount, $credit_card, $return_url, $cancel_url);
+
+ // translate response codes into proper recurring terms
+ if ($response['response_code'] == 1) {
+ // set order OK
+ $this->CI->charge_model->SetStatus($order_id, 1);
+
+ $response['response_code'] = 100;
+ $response['recurring_id'] = $subscription_id;
+ }
+ }
+ else {
+ // this is a free subscription
+ if ($charge_today === TRUE) {
+ // create a $0 order for today's payment
+ $this->CI->load->model('billing/charge_model');
+ $customer['customer_id'] = (isset($customer['customer_id'])) ? $customer['customer_id'] : FALSE;
+ $order_id = $this->CI->charge_model->CreateNewOrder($gateway['gateway_id'], 0, $credit_card, $subscription_id, $customer['customer_id'], $customer_ip);
+ $this->CI->charge_model->SetStatus($order_id, 1);
+ $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id);
+ }
+ else {
+ $response_array = array('recurring_id' => $subscription_id);
+ }
+
+ $response = $this->CI->response->TransactionResponse(100, $response_array);
+ }
+
+ if (isset($created_customer) and $created_customer == TRUE and $response['response_code'] != 100) {
+ // charge was rejected, so let's delete the customer record we just created
+ $this->CI->customer_model->DeleteCustomer($customer['customer_id']);
+ }
+ elseif (isset($created_customer) and $created_customer == TRUE) {
+ $response['customer_id'] = $customer['customer_id'];
+ }
+
+ if ($response['response_code'] != 100) {
+ // clear it out completely
+ $this->CI->recurring_model->DeleteRecurring($subscription_id);
+ }
+
+ if ($response['response_code'] == 100) {
+ // save the "mark_as_renewed" subscription as charge data so we can do this maintenance later
+ // we no longer do this maintenance here because for external gateways, we need to wait
+ // for the user to complete their payment
+ if (!empty($mark_as_renewed)) {
+ $this->CI->load->model('billing/charge_data_model');
+ $this->CI->charge_data_model->Save('r' . $subscription_id, 'mark_as_renewed', $mark_as_renewed);
+ }
+
+ if (!isset($response['not_completed']) or $response['not_completed'] == FALSE) {
+ $this->CI->recurring_model->SetActive($subscription_id);
+
+ // delayed recurrings don't have a charge ID
+ $response['charge_id'] = (isset($response['charge_id'])) ? $response['charge_id'] : FALSE;
+
+ // hook call
+ $this->app_hooks->data('subscription', $response['recurring_id']);
+
+ if (!empty($mark_as_renewed)) {
+ $this->app_hooks->trigger('subscription_renew', $response['recurring_id']);
+ }
+ else {
+ $this->app_hooks->trigger('subscription_new', $response['recurring_id']);
+ }
+
+ // trip a recurring charge?
+ if (!empty($response['charge_id'])) {
+ // hook
+ $this->app_hooks->data('invoice', $response['charge_id']);
+ $this->app_hooks->trigger('subscription_charge', $response['charge_id'], $response['recurring_id']);
+ }
+ }
+ else {
+ unset($response['not_completed']);
+ }
+ }
+ else {
+ // did we require an IP address?
+ if ($gateway_settings['requires_customer_ip'] == 1 and !$customer_ip) {
+ die($this->response->Error(5019));
+ }
+ }
+
+ $this->app_hooks->reset();
+
+ return $response;
+ }
+
+ /**
+ * Refund
+ *
+ * Refund a charge via the gateway
+ *
+ * @param $charge_id The Charge ID to refund
+ *
+ * @return boolean TRUE upon success
+ */
+ function Refund ($charge_id)
+ {
+ $this->CI->transaction_log->log_event($charge_id, FALSE, 'refund_requested', FALSE, __FILE__, __LINE__);
+
+ // Get the order details
+ $this->CI->load->model('billing/charge_model');
+ $charge = $this->CI->charge_model->GetCharge($charge_id);
+
+ // does the order exist?
+ if (!$charge) {
+ die($this->response->Error(4001));
+ }
+
+ // Get the gateway info to load the proper library
+ $this->CI->load->model('billing/gateway_model');
+ $gateway = $this->CI->gateway_model->GetGatewayDetails($charge['gateway_id']);
+
+ // does the gateway exist?
+ if (!$gateway or $gateway['enabled'] == '0') {
+ die($this->response->Error(5017));
+ }
+
+ // load the gateway
+ $gateway_name = $gateway['name'];
+ $this->load->library('billing/payment/'.$gateway_name);
+ $gateway_settings = $this->$gateway_name->Settings();
+
+ $this->CI->transaction_log->log_event($charge_id, FALSE, 'refund_gateway_loaded', array('gateway' => $gateway_name), __FILE__, __LINE__);
+
+ // does the gateway allow refunds?
+ if ($gateway_settings['allows_refunds'] == 0) {
+ return FALSE;
+ }
+
+ // Get the order authorization
+ $this->CI->load->model('billing/order_authorization_model');
+ $authorization = $this->CI->order_authorization_model->GetAuthorization($charge['id']);
+
+ // Pass to Gateway
+ $response = $this->$gateway_name->Refund($gateway, $charge, $authorization);
+
+ $this->CI->transaction_log->log_event($charge_id, FALSE, 'refund_response', array('response' => $response), __FILE__, __LINE__);
+
+ if ($response === TRUE) {
+ // update charge as being refunded
+ $this->CI->charge_model->MarkRefunded($charge_id);
+ }
+
+ return $response; // either TRUE or FALSE
+ }
+
+ /**
+ * Update Credit Card
+ *
+ * Updates the credit card on a subscription. In actuality, it cancels the current subscription and creates a new one.
+ *
+ * @param int $recurring_id
+ * @param array $credit_card The credit card information
+ * @param int $credit_card['card_num'] The credit card number
+ * @param int $credit_card['exp_month'] The credit card expiration month in 2 digit format (01 - 12)
+ * @param int $credit_card['exp_year'] The credit card expiration year (YYYY)
+ * @param string $credit_card['name'] The credit card cardholder name. Required only is customer ID is not supplied.
+ * @param int $credit_card['cvv'] The Card Verification Value. Optional
+ * @param int $gateway_id Set to a gateway_id to use a new gateway for this charge
+ * @param int $new_plan_id Set to a new plan_id if you want to change plans
+ *
+ * @return array With recurring_id, response_code and response_text
+ */
+ function UpdateCreditCard ($recurring_id, $credit_card = array(), $gateway_id = FALSE, $new_plan_id = FALSE) {
+ $this->load->library('field_validation');
+
+ $this->CI->transaction_log->log_event(FALSE, $recurring_id, 'update_requested', FALSE, __FILE__, __LINE__);
+
+ // validate credit card
+ if (!empty($credit_card)) {
+ $credit_card['card_num'] = trim(str_replace(array(' ','-'),'',$credit_card['card_num']));
+ $credit_card['card_type'] = $this->field_validation->ValidateCreditCard($credit_card['card_num']);
+
+ if (!isset($credit_card['card_type']) or empty($credit_card['card_type'])) {
+ die($this->response->Error(5008));
+ }
+
+ if (!isset($credit_card['exp_month']) or empty($credit_card['exp_month'])) {
+ die($this->response->Error(5008));
+ }
+
+ if (!isset($credit_card['exp_year']) or empty($credit_card['exp_year'])) {
+ die($this->response->Error(5008));
+ }
+ }
+ else {
+ die($this->response->Error(5008));
+ }
+
+ // make sure subscription is owned by client
+ // get subscription information
+ $this->CI->load->model('recurring_model');
+ $recurring = $this->CI->recurring_model->GetRecurring($recurring_id);
+
+ $this->CI->transaction_log->log_event(FALSE, $recurring_id, 'update_recurring_retrieved', $recurring, __FILE__, __LINE__);
+
+ // make sure subscription is active
+ if ($recurring['status'] != 'active') {
+ die($this->response->Error(5000));
+ }
+
+ // make sure the subscription isn't free (i.e. that it requires info)
+ if ((float)$recurring['amount'] == 0) {
+ die($this->response->Error(5022));
+ }
+
+ // get the gateway info to load the proper library
+ $gateway = $this->GetGatewayDetails($recurring['gateway_id']);
+
+ if (!$gateway or $gateway['enabled'] == '0') {
+ die($this->response->Error(5017));
+ }
+
+ // load the gateway
+ $gateway_name = $gateway['name'];
+ $this->CI->load->library('payment/'.$gateway_name);
+ $gateway_settings = $this->$gateway_name->Settings();
+
+ $gateway_old = $gateway;
+
+ // calculate end date from CC expiry date and setting for maximum subscription length
+ // calculate the end_date based on the max end date setting
+ $end_date = date('Y-m-d', time() + ($this->config->item('max_recurring_days_from_today') * 86400));
+
+ // if the credit card expiration date is before the end date, we need to set the end date to one day before the expiration
+ $check_year = ($credit_card['exp_year'] > 2000) ? $credit_card['exp_year'] : '20' . $credit_card['exp_year'];
+ $expiry = mktime(0,0,0, $credit_card['exp_month'], days_in_month($credit_card['exp_month'], $credit_card['exp_year']), $check_year);
+
+ if ($expiry < strtotime($end_date)) {
+ // make the adjustment, this card will expire
+ $end_date = mktime(0,0,0, $credit_card['exp_month'], (days_in_month($credit_card['exp_month'], $credit_card['exp_year']) - 1), $credit_card['exp_year']);
+ $end_date = date('Y-m-d', $end_date);
+ }
+
+ // are we using a new gateway?
+ if ($gateway_id != FALSE) {
+ $gateway_new = $this->GetGatewayDetails($gateway_id);
+
+ if (!$gateway_new or $gateway_new['enabled'] == '0') {
+ die($this->response->Error(5017));
+ }
+
+ // load the gateway
+ $gateway_name = $gateway_new['name'];
+ $this->CI->load->library('payment/'.$gateway_name);
+ $gateway_settings = $this->$gateway_name->Settings();
+
+ // does this gateway require customer info we don't have?
+ if ($gateway_settings['requires_customer_information'] == 1 and (!isset($recurring['customer']) or empty($recurring['customer']['address_1']))) {
+ die($this->response->Error(5023));
+ }
+
+ $gateway = $gateway_new;
+ }
+
+ // get new sub start date from $next_charge_date
+ $start_date = date('Y-m-d', strtotime($recurring['next_charge_date']));
+
+ // is this for a plan?
+ $plan_id = (isset($recurring['plan']['id'])) ? $recurring['plan']['id'] : FALSE;
+
+ // save new subscription record
+ $card_last_four = (isset($credit_card['card_num'])) ? substr($credit_card['card_num'],-4,4) : '0';
+
+ // should we modify recurring info based on a new plan? or use the old info?
+ if (!empty($new_plan_id) and $new_plan_id != $recurring['plan']['id']) {
+ $this->CI->load->model('plan_model');
+ $plan_details = $this->CI->plan_model->GetPlanDetails($new_plan_id);
+ }
+ else {
+ $plan_details = FALSE;
+ }
+
+ $recur_amount = (!empty($plan_details)) ? $plan_details->amount : $recurring['amount'];
+ $recur_interval = (!empty($plan_details)) ? $plan_details->interval : $recurring['interval'];
+ $recur_occurrences = (!empty($plan_details)) ? $plan_details->occurrences : $recurring['number_occurrences'];
+ $recur_notification_url = (!empty($plan_details)) ? $plan_details->notification_url : $recurring['notification_url'];
+ $recur_plan_id = (!empty($plan_details)) ? $plan_details->plan_id : $plan_id;
+
+ $subscription_id = $this->CI->recurring_model->SaveRecurring($gateway['gateway_id'], $recurring['customer']['id'], $recur_interval, $start_date, $end_date, $start_date, $recur_occurrences, $recur_notification_url, $recur_amount, $recur_plan_id, $card_last_four);
+
+ // get subscription
+ $subscription = $this->CI->recurring_model->GetRecurring($subscription_id);
+ // is there a charge for today?
+ $charge_today = (date('Y-m-d', strtotime($subscription['date_created'])) == date('Y-m-d', strtotime($subscription['start_date']))) ? TRUE : FALSE;
+
+ // try creating a new subscription
+ $response = $this->CI->$gateway_name->Recur($gateway, $recurring['customer'], $recur_amount, $charge_today, $start_date, $end_date, $recur_interval, $credit_card, $subscription_id, $recur_occurrences, FALSE, FALSE);
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'update_new_subscription_response', array('response' => $response, 'updating_subscription' => $recurring_id), __FILE__, __LINE__);
+
+ if ($response['response_code'] != 100) {
+ // clear it out completely
+ $this->CI->recurring_model->DeleteRecurring($subscription_id);
+
+ // set response code to update CC error
+ $response['response_code'] = '105';
+ return $response;
+ }
+ else {
+ // set active
+ $this->CI->recurring_model->SetActive($subscription_id);
+
+ // mark the old subscription as updated
+ // old ID, new ID
+ $this->CI->recurring_model->SetUpdated($recurring_id, $subscription_id);
+
+ // cancel the old subscription
+ // use $gateway_old for gateway array if we need it
+
+ // by setting $expiring to TRUE, we don't trigger any email triggers
+ $this->CI->recurring_model->CancelRecurring($recurring_id, TRUE);
+
+ // prep the response back
+ $response['recurring_id'] = $subscription_id;
+
+ // set response code to update CC success
+ $response['response_code'] = '104';
+ return $response;
+ }
+ }
+
+ /**
+ * Process a credit card recurring charge
+ *
+ * Processes a credit card CHARGE transaction for a recurring subscription using the gateway_id to use the proper client gateway.
+ * Returns an array response from the appropriate payment library
+ *
+ * @param array $params The subscription array, from GetSubscription, for the recurring charge
+ *
+ * @return mixed Array with response_code and response_text
+ */
+
+ function ChargeRecurring($params)
+ {
+ $gateway_id = $params['gateway_id'];
+
+ $this->CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'recurring_charge_requested', FALSE, __FILE__, __LINE__);
+
+ // Get the gateway info to load the proper library
+ $gateway = $this->GetGatewayDetails($gateway_id);
+
+ if (!$gateway or $gateway['enabled'] == '0') {
+ // we'll cancel the subscription, now
+ $this->CI->load->model('billing/subscription_model');
+ $this->CI->subscription_model->cancel_subscription($params['subscription_id']);
+ return FALSE;
+ }
+
+ // get the credit card last four digits
+ $params['credit_card']['card_num'] = $params['card_last_four'];
+
+ // Create a new order
+ $this->CI->load->model('billing/charge_model');
+ $order_id = $this->CI->charge_model->CreateNewOrder($params['gateway_id'], $params['amount'], $params['credit_card'], $params['subscription_id'], $params['customer_id']);
+
+ $this->CI->transaction_log->log_event(FALSE, $params['subscription_id'], 'recurring_charge_order_created', array('order_id' => $order_id), __FILE__, __LINE__);
+
+ if ((float)$params['amount'] > 0) {
+ // Load the proper library
+ $gateway_name = $gateway['name'];
+ $this->load->library('billing/payment/'.$gateway_name);
+
+ // send to gateway for charging
+ // gateway responds with:
+ // success as TRUE or FALSE
+ // reason (error if success == FALSE)
+ // next_charge (if standard next_charge won't apply)
+ $response = $this->$gateway_name->AutoRecurringCharge($order_id, $gateway, $params);
+ }
+ else {
+ $response = array();
+ $response['success'] = TRUE;
+ }
+
+ $this->CI->transaction_log->log_event($order_id, $params['subscription_id'], 'recurring_charge_response', $response, __FILE__, __LINE__);
+
+ $this->CI->load->model('billing/recurring_model');
+ if ($response['success'] == TRUE) {
+ // Save the last_charge and next_charge
+ $last_charge = date('Y-m-d');
+
+ if (!isset($response['next_charge'])) {
+ $next_charge = $this->CI->recurring_model->GetNextChargeDate($params['subscription_id'], $params['next_charge']);
+ }
+ else {
+ $next_charge = $response['next_charge'];
+ }
+
+ $this->CI->recurring_model->SetChargeDates($params['subscription_id'], $last_charge, $next_charge);
+
+ $this->CI->charge_model->SetStatus($order_id, 1);
+
+ $this->app_hooks->data('invoice', $order_id);
+ $this->app_hooks->data('subscription', $params['subscription_id']);
+ $this->app_hooks->trigger('subscription_charge', $order_id, $params['subscription_id']);
+ $this->app_hooks->reset();
+ } else {
+ $response = FALSE;
+
+ // Check the number of failures allowed
+ $num_allowed = $this->config->item('recurring_charge_failures_allowed');
+ $failures = $params['number_charge_failures'];
+
+ $this->CI->charge_model->SetStatus($order_id, 0);
+
+ $this->app_hooks->data('subscription', $params['subscription_id']);
+ $this->app_hooks->trigger('subscription_renewal_failure', $params['subscription_id']);
+ $this->app_hooks->reset();
+
+ $failures++;
+ $this->CI->recurring_model->AddFailure($params['subscription_id'], $failures);
+
+ if ($failures >= $num_allowed) {
+ $this->CI->load->model('billing/subscription_model');
+ $this->CI->subscription_model->cancel_subscription($params['subscription_id']);
+ }
+ }
+
+ return $response;
+ }
+
+
+ /**
+ * Set a default gateway.
+ *
+ * Sets a provided gateway_id as the default gateway for that client.
+ *
+ * @param int $gateway_id The gateway_id to be set as default.
+ *
+ * @return bool True on success, FALSE on failure
+ */
+ function MakeDefaultGateway($gateway_id)
+ {
+ $this->settings_model->update_setting('default_gateway',$gateway_id);
+ }
+
+ /**
+ * Update the Gateway
+ *
+ * Updates the gateway_params with supplied details
+ *
+ * @param int $params['gateway_id'] The gateway ID to update
+ * @param int $params['accept_mc'] Whether the gateway will accept Mastercard
+ * @param int $params['accept_visa'] Whether the gateway will accept Visa
+ * @param int $params['accept_amex'] Whether the gateway will accept American Express
+ * @param int $params['accept_discover'] Whether the gateway will accept Discover
+ * @param int $params['accept_dc'] Whether the gateway will accept Diner's Club
+ * @param int $params['enabled'] Whether the gateway is enabled or disabled
+ *
+ * @return bool TRUE on success, FALSE on fail.
+ */
+
+ function UpdateGateway($params)
+ {
+ // get gateway details
+ $gateway = $this->GetGatewayDetails($params['gateway_id']);
+
+ if(!$gateway) {
+ die($this->response->Error(3000));
+ }
+
+ // Validate the required fields
+ $this->load->library('billing/payment/'.$gateway['name'], $gateway['name']);
+ $settings = $this->$gateway['name']->Settings();
+ $required_fields = $settings['required_fields'];
+ $this->load->library('field_validation');
+ $validate = $this->field_validation->ValidateRequiredGatewayFields($required_fields, $params);
+
+ $this->load->library('encrypt');
+
+ // manually handle "enabled" and "alias"
+ if (isset($params['enabled']) and ($params['enabled'] == '0' or $params['enabled'] == '1')) {
+ $update_data['enabled'] = $params['enabled'];
+ $this->db->where('gateway_id', $params['gateway_id']);
+ $this->db->update('gateways', $update_data);
+ unset($update_data);
+ }
+
+ if (isset($params['alias']) and !empty($params['alias'])) {
+ $update_data['alias'] = $params['alias'];
+ $this->db->where('gateway_id', $params['gateway_id']);
+ $this->db->update('gateways', $update_data);
+ unset($update_data);
+ }
+
+ $i = 0;
+ foreach($required_fields as $field)
+ {
+ if (isset($params[$field]) and $params[$field] != '') {
+ $update_data['value'] = $this->encrypt->encode($params[$field]);
+ $this->db->where('gateway_id', $params['gateway_id']);
+ $this->db->where('field', $field);
+ $this->db->update('gateway_params', $update_data);
+ $i++;
+ }
+ }
+
+ if ($i === 0) {
+ die($this->response->Error(6003));
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Delete a gateway
+ *
+ * Marks a gateway as deleted and removes the authentication information from the gateway_params table.
+ * Does not actually deleted the gateway, but sets deleted to 1 in the gateways table.
+ *
+ * @param int $gateway_id The gateway_id to be set deleted.
+ *
+ * @return bool TRUE on success
+ */
+
+ function DeleteGateway($gateway_id, $completely = FALSE)
+ {
+ // get gateway details
+ $gateway = $this->GetGatewayDetails($gateway_id);
+
+ if(!$gateway) {
+ die($this->response->Error(3000));
+ }
+
+ // cancel all subscriptions related to it
+ $this->CI->load->model('billing/recurring_model');
+ $subscriptions = $this->CI->recurring_model->GetAllSubscriptionsByGatewayID($gateway_id);
+ if (is_array($subscriptions)) {
+ foreach ($subscriptions as $subscription) {
+ $this->CI->recurring_model->CancelRecurring($subscription['subscription_id']);
+ }
+ }
+
+ // Mark as deleted
+ if ($completely == FALSE) {
+ $update_data['deleted'] = 1;
+ $this->db->where('gateway_id', $gateway_id);
+ $this->db->update('gateways', $update_data);
+ }
+ else {
+ // remove from database completely
+ $this->db->delete('gateways',array('gateway_id' => $gateway_id));
+ }
+
+ // Delete the client gateway params
+ $this->db->where('gateway_id', $gateway_id);
+ $this->db->delete('gateway_params');
+
+ return TRUE;
+ }
+
+ /**
+ * Get the External API ID
+ *
+ * Gets the External API ID from the external_apis table based on the gateway type ('authnet', 'exact' etc.)
+ *
+ * @param string $gateway_name The name to match with External API ID
+ *
+ * @return int External API ID
+ */
+
+ // Get the gateway id
+ function GetExternalApiId($gateway_name = FALSE)
+ {
+ if($gateway_name) {
+ $this->db->where('name', $gateway_name);
+ $query = $this->db->get('external_apis');
+ if($query->num_rows > 0) {
+ return $query->row()->external_api_id;
+ } else {
+ die($this->response->Error(2001));
+ }
+
+ }
+ }
+
+ /**
+ * Get list of current gateways
+ *
+ * Returns details of all gateways for a client.
+ *
+ * @param int $params['deleted'] Whether or not the gateway is deleted. Possible values are 1 for deleted and 0 for active
+ * @param int $params['id'] The email ID. GetGateway could also be used. Optional.
+ * @param int $params['offset']
+ * @param int $params['limit'] The number of records to return. Optional.
+ *
+ * @return mixed Array containg all gateways meeting criteria
+ */
+
+ function GetGateways ($params = array())
+ {
+ if(isset($params['deleted']) and $params['deleted'] == '1') {
+ $this->db->where('gateways.deleted', '1');
+ }
+ else {
+ $this->db->where('gateways.deleted', '0');
+ }
+
+ if (isset($params['offset'])) {
+ $offset = $params['offset'];
+ }
+ else {
+ $offset = 0;
+ }
+
+ if(isset($params['limit'])) {
+ $this->db->limit($params['limit'], $offset);
+ }
+
+ $this->db->join('external_apis', 'external_apis.external_api_id = gateways.external_api_id', 'left');
+
+ $this->db->select('gateways.*');
+ $this->db->select('external_apis.*');
+
+ $query = $this->db->get('gateways');
+ $data = array();
+ if($query->num_rows() > 0) {
+ foreach($query->result_array() as $row)
+ {
+ $array = array(
+ 'id' => $row['gateway_id'],
+ 'gateway' => (!empty($row['alias'])) ? $row['alias'] : $row['display_name'],
+ 'name' => $row['name'],
+ 'enabled' => ($row['enabled'] == '1') ? TRUE : FALSE,
+ 'date_created' => $row['create_date']
+ );
+
+ $data[] = $array;
+ }
+
+ } else {
+ return FALSE;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get the gateway details.
+ *
+ * Returns an array containg all the details for the Client Gateway
+ *
+ * @param int $gateway_id The gateway_id
+ *
+ * @return array All gateway details
+ */
+
+ function GetGatewayDetails($gateway_id = FALSE, $deleted_ok = FALSE)
+ {
+ // If they have not passed a gateway ID, we will choose the first one created.
+ if ($gateway_id) {
+ $this->db->where('gateways.gateway_id', $gateway_id);
+ } elseif (!empty($client->default_gateway_id)) {
+ $this->db->where('gateways.gateway_id', $client->default_gateway_id);
+ } else {
+ $this->db->order_by('create_date', 'ASC');
+ }
+
+ $this->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ if ($deleted_ok == FALSE) {
+ $this->db->where('deleted', 0);
+ }
+ $this->db->limit(1);
+ $query = $this->db->get('gateways');
+ if($query->num_rows > 0) {
+
+ $row = $query->row();
+ $data = array();
+ $data['url_live'] = $row->prod_url;
+ $data['url_test'] = $row->test_url;
+ $data['url_dev'] = $row->dev_url;
+ $data['arb_url_live'] = $row->arb_prod_url;
+ $data['arb_url_test'] = $row->arb_test_url;
+ $data['arb_url_dev'] = $row->arb_dev_url;
+ $data['name'] = $row->name;
+ $data['alias'] = $row->alias;
+ $data['enabled'] = $row->enabled;
+ $data['gateway_id'] = $row->gateway_id;
+
+ // Get the params
+ $this->load->library('encrypt');
+ $this->db->where('gateway_id', $row->gateway_id);
+ $query = $this->db->get('gateway_params');
+ if($query->num_rows() > 0) {
+ foreach($query->result() as $row) {
+ $data[$row->field] = $this->encrypt->decode($row->value);
+ }
+ }
+
+ return $data;
+ } else {
+ die($this->response->Error(3000));
+ }
+ }
+
+ /**
+ * Get available Gateway External API's
+ *
+ * Loads a list of all possible gateway types, as well as their required fields
+ *
+ * @return array|bool Returns an array containing all of the fields required for that gateway type or FALSE upon failure
+ */
+ function GetExternalAPIs()
+ {
+ $this->db->order_by('display_name');
+
+ $query = $this->db->get('external_apis');
+ if($query->num_rows() > 0) {
+ $gateways = array();
+
+ foreach ($query->result_array() as $row) {
+ $this->load->library('billing/payment/' . strtolower($row['name']),$row['name']);
+
+ $settings = $this->$row['name']->Settings();
+
+ $gateways[] = $settings;
+ }
+
+ return $gateways;
+ } else {
+ return FALSE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/invoice_model.php b/app/modules/billing/models/invoice_model.php
new file mode 100644
index 00000000..a6ce152d
--- /dev/null
+++ b/app/modules/billing/models/invoice_model.php
@@ -0,0 +1,464 @@
+get_invoice($invoice_id);
+
+ $CI =& get_instance();
+
+ // holds the lines
+ $lines = array();
+
+ // Deal with any sub-modules that might have put the order in here...
+ if (isset($invoice['module']) && !empty($invoice['module']))
+ {
+ $cname = $invoice['module'] .'_lib';
+ $CI->load->library($invoice['module'] .'/'. $cname);
+
+ $lines = $CI->$cname->invoice_lines($invoice);
+ }
+
+ // build subscription line, with tax
+ else if (!empty($invoice['subscription_id'])) {
+ // get subscription information
+ $CI->load->model('billing/subscription_model');
+ $subscription = $CI->subscription_model->get_subscription($invoice['subscription_id']);
+
+ // is there a tax here?
+ $CI->load->model('store/taxes_model');
+ $tax = $CI->taxes_model->get_tax_for_subscription($subscription['id']);
+
+ // get the proper amount for the subscription (initial charge or not)
+ $this->load->model('billing/charge_data_model');
+
+ // we'll check for charge_data
+ // this means that this is the initial charge
+ // this is the most accurate means of getting the initial subscription price because
+ // coupons, etc. may be augmenting it
+ if ($charge_data = $this->charge_data_model->get($invoice['id'])) {
+ $totals = unserialize($charge_data['totals']);
+ $sub_price = $totals['recurring_sub_total'];
+ }
+ else {
+ $sub_price = $subscription['amount'];
+
+ // subtract tax from this?
+ // we only do this when we aren't using the initial charge because the initial charge
+ // is the raw value from the plan while this is the actual charging value
+ if ($tax['tax_amount'] !== FALSE) {
+ $sub_price = $sub_price - $tax['tax_amount'];
+ }
+ }
+
+ $lines[] = array(
+ 'line' => '(Subscription) ' . $subscription['plan']['name'],
+ 'quantity' => '1',
+ 'sub_total' => money_format("%!^i",$sub_price)
+ );
+ }
+
+ // build lines from products
+ if (!empty($invoice['order_details_id'])) {
+ $result = $this->db->select('products.*')
+ ->select('order_products.*')
+ ->from('order_products')
+ ->join('products','products.product_id = order_products.product_id')
+ ->where('order_details_id',$invoice['order_details_id'])
+ ->get();
+
+ if ($result->num_rows() > 0) {
+ foreach ($result->result_array() as $product) {
+ $line = $product['product_name'];
+
+ // do we have product options to show?
+ if (!empty($product['order_products_options']) and $product['order_products_options'] != serialize(array())) {
+ $product['order_products_options'] = unserialize($product['order_products_options']);
+
+ $line .= ' (';
+
+ foreach ($product['order_products_options'] as $label => $value) {
+ $line .= $label . ': ' . $value . ', ';
+ }
+
+ $line = rtrim($line, ', ');
+
+ $line .= ')';
+ }
+
+ $lines[] = array(
+ 'line' => $line,
+ 'quantity' => $product['order_products_quantity'],
+ 'original_price' => $product['product_price'],
+ 'sub_total' => $product['order_products_price'],
+ 'shipped' => ($product['order_products_shipped'] == '1') ? TRUE : FALSE,
+ 'order_products_id' => $product['order_products_id'],
+ 'product_options' => $product['order_products_options'],
+ 'product_name' => $product['product_name'],
+ 'sku' => $product['product_sku']
+ );
+ }
+ }
+ }
+
+ return $lines;
+ }
+
+ /**
+ * Get Invoice Data
+ *
+ * @param int $invoice_id
+ *
+ * @return array $data
+ */
+ function get_invoice_data ($invoice_id) {
+ $CI =& get_instance();
+
+ $CI->load->model('billing/charge_data_model');
+ $charge_data = $CI->charge_data_model->get($invoice_id);
+
+ $charge_data = unserialize($charge_data['totals']);
+
+ $data = array(
+ 'shipping' => money_format("%!^i",$charge_data['shipping']),
+ 'subtotal' => money_format("%!^i",$charge_data['order_sub_total']),
+ 'tax' => money_format("%!^i",$charge_data['order_tax']),
+ 'total' => money_format("%!^i",$charge_data['order_total']),
+ 'discount' => money_format("%!^i",$charge_data['discount'])
+ );
+
+ return $data;
+ }
+
+ /**
+ * Get Invoice
+ *
+ * @param int $invoice_id
+ *
+ * @return array $invoice
+ */
+ function get_invoice ($invoice_id) {
+ if (isset($this->cache[$invoice_id]) and !empty($this->cache[$invoice_id])) {
+ return $this->cache[$invoice_id];
+ }
+
+ $invoice = $this->get_invoices(array('id' => $invoice_id));
+
+ if (empty($invoice)) {
+ return FALSE;
+ }
+
+ $this->cache[$invoice_id] = $invoice[0];
+ return $invoice[0];
+ }
+
+ /**
+ * Get Invoice Total
+ *
+ * @param int $filters['user_id'] Member ID
+ * @param date $filters['start_date'] Only orders after or on this date will be returned. Optional.
+ * @param date $filters['end_date'] Only orders before or on this date will be returned. Optional.
+ * @param int $filters['id'] The charge ID. Optional.
+ * @param string $filters['amount'] The amount of the charge. Optional.
+ * @param boolean $filters['subscription_id'] Only charges linked to this subscription.
+ * @param int $filters['card_last_four'] Last 4 digits of credit card
+ * @param int $filters['offset'] Offsets the database query.
+ * @param int $filters['limit'] Limits the number of results returned. Optional.
+ * @param string $filters['sort'] Column used to sort the results.
+ * @param string $filters['sort_dir'] Used when a sort param is supplied. Possible values are asc and desc. Optional.
+ * @param boolean $counting Set to TRUE to receive the total number of matching invoices
+ *
+ * @return float $total
+ */
+ function get_invoices_total ($filters)
+ {
+ if (isset($filters['start_date'])) {
+ $start_date = date('Y-m-d H:i:s', strtotime($filters['start_date']));
+ $this->db->where('orders.timestamp >=', $start_date);
+ }
+
+ if (isset($filters['end_date'])) {
+ $end_date = date('Y-m-d H:i:s', strtotime($filters['end_date']));
+ $this->db->where('orders.timestamp <=', $end_date);
+ }
+
+ if(isset($filters['amount'])) {
+ $this->db->where('orders.amount', $filters['amount']);
+ }
+
+ if (isset($filters['id'])) {
+ $this->db->where('orders.order_id', $filters['id']);
+ }
+
+ if (isset($filters['member_name'])) {
+ if (is_numeric($filters['member_name'])) {
+ // we are passed a member id
+ $this->db->where('users.user_id',$filters['member_name']);
+ } else {
+ $this->db->like('users.user_last_name', $filters['member_name']);
+ }
+ }
+
+ if (isset($filters['customer_id'])) {
+ $this->db->where('orders.customer_id', $filters['customer_id']);
+ }
+
+ if (isset($filters['user_id'])) {
+ $this->db->where('customers.internal_id', $filters['user_id']);
+ $this->db->join('customers', 'customers.customer_id = orders.customer_id', 'inner');
+ }
+
+ if (isset($filters['gateway'])) {
+ $this->db->like('gateways.alias', $filters['gateway']);
+ $this->db->join('gateways', 'gateways.gateway_id = orders.gateway_id', 'left');
+ }
+
+ if (isset($filters['subscription_id'])) {
+ $this->db->where('orders.subscription_id', $filters['subscription_id']);
+ }
+
+ if (isset($filters['card_last_four'])) {
+ $this->db->where('orders.card_last_four', $filters['card_last_four']);
+ }
+
+ $this->db->join('customers', 'customers.customer_id = orders.customer_id', 'inner');
+ $this->db->join('users', 'customers.internal_id = users.user_id', 'inner');
+
+ $this->db->where('orders.status','1');
+
+ $this->db->select('orders.amount');
+
+ $this->db->group_by('orders.order_id');
+
+ $result = $this->db->get('orders');
+
+ if ($result->num_rows() == 0) {
+ return 0;
+ }
+
+ $total = 0;
+
+ foreach($result->result_array() as $invoice) {
+ $total += $invoice['amount'];
+ }
+
+ return $total;
+ }
+
+ function count_invoices ($filters = array()) {
+ $invoices = $this->get_invoices($filters, TRUE);
+
+ return $invoices;
+ }
+
+ /**
+ * View Invoices
+ *
+ * Returns an array of results based on submitted search criteria. All fields are optional.
+ *
+ * @param int $filters['user_id'] Member ID
+ * @param date $filters['start_date'] Only orders after or on this date will be returned. Optional.
+ * @param date $filters['end_date'] Only orders before or on this date will be returned. Optional.
+ * @param int $filters['id'] The charge ID. Optional.
+ * @param string $filters['amount'] The amount of the charge. Optional.
+ * @param boolean $filters['subscription_id'] Only charges linked to this subscription.
+ * @param int $filters['card_last_four'] Last 4 digits of credit card
+ * @param int $filters['offset'] Offsets the database query.
+ * @param int $filters['limit'] Limits the number of results returned. Optional.
+ * @param string $filters['sort'] Column used to sort the results.
+ * @param string $filters['sort_dir'] Used when a sort param is supplied. Possible values are asc and desc. Optional.
+ * @param boolean $counting Set to TRUE to receive the total number of matching invoices
+ *
+ * @return array|bool Invoice results or FALSE if none
+ */
+ function get_invoices ($filters, $counting = FALSE)
+ {
+ if (isset($filters['start_date'])) {
+ $start_date = date('Y-m-d H:i:s', strtotime($filters['start_date']));
+ $this->db->where('timestamp >=', $start_date);
+ }
+
+ if (isset($filters['end_date'])) {
+ $end_date = date('Y-m-d H:i:s', strtotime($filters['end_date']));
+ $this->db->where('timestamp <=', $end_date);
+ }
+
+ if(isset($filters['amount'])) {
+ $this->db->where('orders.amount', $filters['amount']);
+ }
+
+ if (isset($filters['id'])) {
+ $this->db->where('orders.order_id', $filters['id']);
+ }
+
+ if (isset($filters['customer_id'])) {
+ $this->db->where('orders.customer_id', $filters['customer_id']);
+ }
+
+ if (isset($filters['user_id'])) {
+ $this->db->where('customers.internal_id', $filters['user_id']);
+ }
+
+ if (isset($filters['gateway'])) {
+ $this->db->like('gateways.gateway_id', $filters['gateway']);
+ }
+
+ if (isset($filters['member_name'])) {
+ if (is_numeric($filters['member_name'])) {
+ // we are passed a member id
+ $this->db->where('users.user_id',$filters['member_name']);
+ } else {
+ $this->db->like('users.user_last_name', $filters['member_name']);
+ }
+ }
+
+ if (isset($filters['subscription_id'])) {
+ $this->db->where('orders.subscription_id', $filters['subscription_id']);
+ }
+
+ if (isset($filters['card_last_four'])) {
+ $this->db->where('orders.card_last_four', $filters['card_last_four']);
+ }
+
+ if (isset($filters['status'])) {
+ $this->db->where('orders.status',$filters['status']);
+ }
+ else {
+ $this->db->where('orders.status','1');
+ }
+
+ if (isset($filters['refunded']) and $filters['refunded'] == TRUE) {
+ $this->db->where('orders.refunded','1');
+ }
+
+ // standard ordering and limiting
+ $order_by = (isset($filters['sort'])) ? $filters['sort'] : 'orders.order_id';
+ $order_dir = (isset($filters['sort_dir'])) ? $filters['sort_dir'] : 'DESC';
+ $this->db->order_by($order_by, $order_dir);
+
+ if (isset($filters['limit'])) {
+ $offset = (isset($filters['offset'])) ? $filters['offset'] : 0;
+ $this->db->limit($filters['limit'], $offset);
+ }
+
+ $this->db->join('customers', 'customers.customer_id = orders.customer_id', 'inner');
+ $this->db->join('users', 'customers.internal_id = users.user_id', 'inner');
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+ $this->db->join('gateways', 'gateways.gateway_id = orders.gateway_id', 'left');
+ $this->db->join('order_details', 'order_details.order_id = orders.order_id','left');
+ $this->db->join('taxes_received', 'orders.order_id = taxes_received.order_id','left');
+ $this->db->join('taxes', 'taxes.tax_id = taxes_received.tax_id','left');
+ $this->db->join('shipping_received', 'orders.order_id = shipping_received.order_id','left');
+ $this->db->join('shipping', 'shipping.shipping_id = shipping_received.shipping_id','left');
+ $this->db->join('coupons', 'order_details.coupon_id = coupons.coupon_id','left');
+
+ $this->db->group_by('orders.order_id');
+
+ if ($counting == TRUE) {
+ $this->db->select('orders.order_id');
+ $result = $this->db->get('orders');
+
+ $total_rows = $result->num_rows();
+ $result->free_result();
+ return $total_rows;
+ }
+ else {
+ $this->db->select('*');
+ $this->db->select('orders.order_id AS invoice_id');
+ $this->db->select('orders.subscription_id AS true_subscription_id');
+ }
+
+ $this->db->from('orders');
+ $result = $this->db->get();
+
+ if ($result->num_rows() == 0) {
+ return FALSE;
+ }
+// die(''. print_r($result->result(), true));
+ $invoices = array();
+
+ foreach($result->result_array() as $invoice) {
+ $t = array(
+ 'id' => $invoice['invoice_id'],
+ 'gateway_id' => $invoice['gateway_id'],
+ 'gateway' => $invoice['alias'],
+ 'date' => local_time($invoice['timestamp']),
+ 'user_id' => $invoice['user_id'],
+ 'user_first_name' => $invoice['user_first_name'],
+ 'user_last_name' => $invoice['user_last_name'],
+ 'user_email' => $invoice['user_email'],
+ 'user_groups' => $invoice['user_groups'],
+ 'amount' => money_format("%!^i",(float)$invoice['amount']),
+ 'refunded' => ($invoice['refunded'] == '1') ? TRUE : FALSE,
+ 'card_last_four' => $invoice['card_last_four'],
+ 'is_refunded' => (empty($invoice['refunded'])) ? FALSE : TRUE,
+ 'refund_date' => (empty($invoice['refunded'])) ? FALSE : local_time($invoice['refund_date']),
+ 'subscription_id' => $invoice['true_subscription_id'],
+ 'tax_name' => (empty($invoice['tax_name'])) ? FALSE : $invoice['tax_name'],
+ 'tax_paid' => money_format("%!^i",(float)($invoice['tax_received_for_products'] + $invoice['tax_received_for_subscription'])),
+ 'tax_rate' => (empty($invoice['tax_percentage'])) ? FALSE : $invoice['tax_percentage'],
+ 'shipping_id' => (!empty($invoice['shipping_id'])) ? $invoice['shipping_id'] : FALSE,
+ 'shipping_name' => (!empty($invoice['shipping_name'])) ? $invoice['shipping_name'] : 'Default',
+ 'shipping_charge' => (!empty($invoice['shipping_received_amount'])) ? money_format("%!^i", (float)$invoice['shipping_received_amount']) : FALSE,
+ 'order_details_id' => (!empty($invoice['order_details_id'])) ? $invoice['order_details_id'] : FALSE,
+ 'coupon_name' => (!empty($invoice['coupon_name'])) ? $invoice['coupon_name'] : FALSE,
+ 'coupon_code' => (!empty($invoice['coupon_code'])) ? $invoice['coupon_code'] : FALSE,
+ 'coupon_id' => (!empty($invoice['coupon_id'])) ? $invoice['coupon_id'] : FALSE,
+ 'billing_address' => array(
+ 'first_name' => $invoice['first_name'],
+ 'last_name' => $invoice['last_name'],
+ 'company' => $invoice['company'],
+ 'address_1' => $invoice['address_1'],
+ 'address_2' => $invoice['address_2'],
+ 'city' => $invoice['city'],
+ 'state' => $invoice['state'],
+ 'country' => $invoice['iso2'],
+ 'postal_code' => $invoice['postal_code'],
+ 'email' => $invoice['email'],
+ 'phone_number' => $invoice['phone']
+ ),
+ );
+
+ // If we're using a dynamic shipping module, then we might
+ // have a better description of the name we can use.
+ if (isset($invoice['shipping_desc']) && !empty($invoice['shipping_desc']))
+ {
+ $t['shipping_name'] = $invoice['shipping_desc'];
+ }
+
+ // If there's any chance a module was associated with this order, send that along
+ if (isset($invoice['module']) && !empty($invoice['module']))
+ {
+ $t['module'] = $invoice['module'];
+ }
+
+ $invoices[] = $t;
+ }
+
+ return $invoices;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/order_authorization_model.php b/app/modules/billing/models/order_authorization_model.php
new file mode 100644
index 00000000..f7bde9b2
--- /dev/null
+++ b/app/modules/billing/models/order_authorization_model.php
@@ -0,0 +1,70 @@
+CI =& get_instance();
+ $this->CI->load->library('billing/transaction_log');
+ }
+
+ /**
+ * Save order authorization details.
+ *
+ * Save the order authorization number returned from the payment gateway
+ *
+ * @param int $order_id The order ID
+ * @param string $tran_id Transaction ID. Optional
+ * @param string $authorization_code Authorization code. Optional
+ *
+ */
+ function SaveAuthorization($order_id, $tran_id = '', $authorization_code = '', $security_key = '')
+ {
+ $insert_data = array(
+ 'order_id' => (!empty($order_id)) ? $order_id : '',
+ 'tran_id' => (!empty($tran_id)) ? $tran_id : '',
+ 'authorization_code' => (!empty($authorization_code)) ? $authorization_code : '',
+ 'security_key' => (!empty($security_key)) ? $security_key : ''
+ );
+
+ $this->db->insert('order_authorizations', $insert_data);
+
+ $this->CI->transaction_log->log_event($order_id, FALSE, 'authorization_saved', $insert_data, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Get Authorization Details.
+ *
+ * Gets the authorization details for an order_id
+ *
+ * @param int $order_id The order ID
+ *
+ * @return mixed Array containg authorization details
+ */
+
+ function GetAuthorization($order_id)
+ {
+ $this->db->where('order_id', $order_id);
+ $query = $this->db->get('order_authorizations');
+ if($query->num_rows() > 0) {
+ return $query->row();
+ } else {
+ return FALSE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/plan_model.php b/app/modules/billing/models/plan_model.php
new file mode 100644
index 00000000..e5f3ebde
--- /dev/null
+++ b/app/modules/billing/models/plan_model.php
@@ -0,0 +1,425 @@
+load->library('field_validation');
+
+ if (isset($plan['plan_type'])) {
+ $plan_type_id = $this->GetPlanTypeId($plan['plan_type']);
+ $insert_data['plan_type_id'] = $plan_type_id;
+ } else {
+ if (isset($plan['amount']) and (float)$plan['amount'] > 0) {
+ $plan_type_id = $this->GetPlanTypeId('paid');
+ $insert_data['plan_type_id'] = $plan_type_id;
+ }
+ elseif (isset($plan['amount']) and (float)$plan['amount'] == 0) {
+ $plan_type_id = $this->GetPlanTypeId('free');
+ $insert_data['plan_type_id'] = $plan_type_id;
+ }
+ else {
+ die($this->response->Error(1004));
+ }
+ }
+
+ if (isset($plan['plan_type']) and $plan['plan_type'] == 'free') {
+ $insert_data['amount'] = 0;
+ } else {
+ if(isset($plan['amount'])) {
+ if(!$this->field_validation->ValidateAmount($plan['amount'])) {
+ die($this->response->Error(5009));
+ }
+ $insert_data['amount'] = $plan['amount'];
+ } else {
+ die($this->response->Error(1004));
+ }
+ }
+
+ if (isset($plan['interval'])) {
+ if(!is_numeric($plan['interval']) || $plan['interval'] < 1) {
+ die($this->response->Error(5011));
+ }
+ $insert_data['`interval`'] = $plan['interval'];
+ } else {
+ die($this->response->Error(1004));
+ }
+
+ if (isset($plan['notification_url'])) {
+ $insert_data['notification_url'] = $plan['notification_url'];
+ }
+
+ if (isset($plan['name'])) {
+ $insert_data['name'] = $plan['name'];
+ } else {
+ die($this->response->Error(1004));
+ }
+
+ if (isset($plan['occurrences'])) {
+ if (!is_numeric($plan['occurrences'])) {
+ die($this->response->Error(7003));
+ }
+ else {
+ $insert_data['occurrences'] = $plan['occurrences'];
+ }
+ }
+ else {
+ $insert_data['occurrences'] = '0';
+ }
+
+ if (isset($plan['free_trial'])) {
+ if(!is_numeric($plan['free_trial']) || $plan['free_trial'] < 0) {
+ die($this->response->Error(7002));
+ }
+ $insert_data['free_trial'] = $plan['free_trial'];
+ } else {
+ $insert_data['free_trial'] = '0';
+ }
+
+ $insert_data['deleted'] = 0;
+
+ if ($this->db->insert('plans', $insert_data)) {
+ return $this->db->insert_id();
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /*
+ * Updates a plan
+ *
+ * @param int $params['plan_id'] The ID of the plan being updated
+ * @param string $params['plan_type'] The name of the plan type (e.g. "paid"). Optional.
+ * @param string $params['amount'] Charge amount. Optional.
+ * @param int $params['interval'] Interval (days). Optional.
+ * @param string $params['notification_url'] Notification URL. Optional.
+ * @param string $params['name'] Name. Optional.
+ * @param int $params['occurrences']. Number of occurrences. Optional.
+ * @param int $params['free_trial'] Number of days of a free trial. Optional.
+ *
+ * @return bool Returns TRUE on success or FALSE on failure
+ */
+ function UpdatePlan($plan_id, $params)
+ {
+ // Get the plan params
+ $plan = $params;
+
+ // Get the plan details
+ $plan_details = $this->GetPlanDetails($plan_id);
+
+ $this->load->library('field_validation');
+
+ if (isset($plan['plan_type'])) {
+ $plan_type_id = $this->GetPlanTypeId($plan['plan_type']);
+ $update_data['plan_type_id'] = $plan_type_id;
+ }
+
+ if (isset($plan['plan_type']) and $plan['plan_type'] == 'free') {
+ $update_data['amount'] = 0;
+ } else {
+ if(isset($plan['amount'])) {
+ if(!$this->field_validation->ValidateAmount($plan['amount'])) {
+ die($this->response->Error(5009));
+ }
+ $update_data['amount'] = $plan['amount'];
+ }
+ }
+
+ if(isset($plan['interval'])) {
+ if(!is_numeric($plan['interval']) || $plan['interval'] < 1) {
+ die($this->response->Error(5011));
+ }
+ $update_data['`interval`'] = $plan['interval'];
+ }
+
+ if(isset($plan['notification_url'])) {
+ $update_data['notification_url'] = $plan['notification_url'];
+ }
+
+ if(isset($plan['name'])) {
+ $update_data['name'] = $plan['name'];
+ }
+
+ if(isset($plan['occurrences'])) {
+ if (!is_numeric($plan['occurrences'])) {
+ die($this->response->Error(7003));
+ }
+ else {
+ $update_data['occurrences'] = $plan['occurrences'];
+ }
+ }
+
+ if(isset($plan['free_trial'])) {
+ if(!is_numeric($plan['free_trial']) || $plan['free_trial'] < 0) {
+ die($this->response->Error(7002));
+ }
+ $update_data['free_trial'] = $plan['free_trial'];
+ }
+
+ if(!isset($update_data)) {
+ die($this->response->Error(6003));
+ }
+
+ $this->db->where('plan_id', $plan_details->plan_id);
+ if ($this->db->update('plans', $update_data)) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /*
+ * Get plan information by plan ID
+ *
+ * @param int $plan_id The Plan ID
+ *
+ * @return array Plan information
+ *
+ */
+ function GetPlan($plan_id)
+ {
+ $params = array('plan_id' => $plan_id);
+
+ $data = $this->GetPlans($params);
+
+ if (!empty($data)) {
+ return $data[0];
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /*
+ * Gets a list of all active plans with optional filters
+ *
+ * @param string $params['plan_type'] The name of the plan type (e.g. "paid"). Optional.
+ * @param string $params['amount'] Amount filter. Optional.
+ * @param int $params['interval'] Interval (days) filter. Optional.
+ * @param string $params['notification_url'] Notification URL filter. Optional.
+ * @param string $params['name'] Name filter. Optional.
+ * @param int $params['free_trial'] Number of days of a free trial. Optional.
+ * @param int $params['deleted'] Set to 1 for deleted plans. Optional.
+ * @param int $params['offset'] Database query offset. Optional. Defaults to 0.
+ * @param int $params['limit'] Database query limit. Optional.'
+ *
+ * @return array|bool Returns an array of all plans matching query or FALSE if none.
+ */
+ function GetPlans($params = array())
+ {
+ if(isset($params['plan_type'])) {
+ $plan_type_id = $this->GetPlanTypeId($params['plan_type']);
+ $this->db->where('plans.plan_type_id', $plan_type_id);
+ }
+
+ if(isset($params['deleted']) and $params['deleted'] == '1') {
+ $this->db->where('deleted', '1');
+ }
+ else {
+ $this->db->where('deleted', '0');
+ }
+
+ if(isset($params['plan_id'])) {
+ $this->db->where('plans.plan_id', $params['plan_id']);
+ }
+
+ if(isset($params['amount'])) {
+ $this->db->where('plans.amount', $params['amount']);
+ }
+
+ if(isset($params['interval'])) {
+ $this->db->where('interval', $params['interval']);
+ }
+
+ if(isset($params['notification_url'])) {
+ $this->db->where('notification_url', $params['notification_url']);
+ }
+
+ if(isset($params['name'])) {
+ $this->db->where('name', $params['name']);
+ }
+
+ if(isset($params['free_trial'])) {
+ $this->db->where('free_trial', $params['free_trial']);
+ }
+
+ if (isset($params['offset'])) {
+ $offset = $params['offset'];
+ }
+ else {
+ $offset = 0;
+ }
+
+ if(isset($params['limit'])) {
+ $this->db->limit($params['limit'], $offset);
+ }
+
+ if(isset($params['sort_dir']) and ($params['sort_dir'] == 'asc' or $params['sort_dir'] == 'desc' )) {
+ $sort_dir = $params['sort_dir'];
+ }
+ else {
+ $sort_dir = 'desc';
+ }
+
+ $params['sort'] = isset($params['sort']) ? $params['sort'] : '';
+
+ switch($params['sort'])
+ {
+ case 'id':
+ $sort = 'plans.plan_id';
+ break;
+ case 'amount':
+ $sort = 'amount';
+ break;
+ default:
+ $sort = 'plans.plan_id';
+ break;
+ }
+ $this->db->order_by($sort, $sort_dir);
+
+ $this->db->select('plans.*');
+ $this->db->select('plan_types.*');
+ $this->db->select('SUM(subscriptions.active) as `num_customers`',false);
+ $this->db->join('subscriptions','subscriptions.plan_id = plans.plan_id','left');
+ $this->db->join('plan_types', 'plans.plan_type_id = plan_types.plan_type_id', 'inner');
+ $this->db->group_by('plans.plan_id');
+ $query = $this->db->get('plans');
+
+ $data = array();
+ if($query->num_rows() > 0) {
+ foreach($query->result() as $row)
+ {
+ $data[] = array(
+ 'id' => $row->plan_id,
+ 'type' => $row->type,
+ 'name' => $row->name,
+ 'amount' => money_format("%!^i",$row->amount),
+ 'interval' => $row->interval,
+ 'notification_url' => $row->notification_url,
+ 'free_trial' => $row->free_trial,
+ 'occurrences' => $row->occurrences,
+ 'num_customers' => (empty($row->num_customers)) ? '0' : $row->num_customers,
+ 'status' => ($row->deleted == '0') ? 'active' : 'deleted'
+ );
+ }
+
+ } else {
+ return FALSE;
+ }
+
+ return $data;
+ }
+
+ /*
+ * Marks a plan as deleted
+ *
+ * @param int $plan_id The ID of the plan
+ *
+ * @return bool TRUE upon success
+ *
+ */
+ function DeletePlan($plan_id)
+ {
+ // Get the plan details
+ $plan_details = $this->GetPlanDetails($plan_id);
+
+ $update_data['deleted'] = 1;
+ $this->db->where('plan_id', $plan_details->plan_id);
+ $this->db->update('plans', $update_data);
+
+ return TRUE;
+ }
+
+ /**
+ * Verifies that the plan exists, is available to the client, and is active
+ *
+ * @param int $plan_id The Plan ID
+ *
+ * @return array Plan information
+ */
+ function GetPlanDetails($plan_id)
+ {
+ $this->db->where('plan_id', $plan_id);
+ $this->db->where('deleted', 0);
+ $query = $this->db->get('plans');
+ if($query->num_rows() > 0) {
+ return $query->row();
+ } else {
+ die($this->response->Error(7001));
+ }
+ }
+
+ /**
+ * Gets the ID number for a plan type by plan type namme
+ *
+ * @param string $type The name of the plan type
+ *
+ * @return int The ID of the plan type
+ */
+ function GetPlanTypeId($type)
+ {
+ $this->db->where('type', $type);
+ $query = $this->db->get('plan_types');
+ if($query->num_rows() > 0) {
+ $plan_type_id = $query->row()->plan_type_id;
+ } else {
+ die($this->response->Error(7000));
+ }
+
+ return $plan_type_id;
+ }
+
+ /**
+ * Gets the plan type name by the plan type ID
+ *
+ * @param int $plan_type_id The ID of the plan type
+ *
+ * @return string The name of the plantype
+ */
+ function GetPlanType($plan_type_id)
+ {
+ $this->db->where('plan_type_id', $plan_type_id);
+ $query = $this->db->get('plan_types');
+ if($query->num_rows() > 0) {
+ $plan_type = $query->row()->type;
+ } else {
+ die($this->response->Error(7000));
+ }
+
+ return $plan_type;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/recurring_model.php b/app/modules/billing/models/recurring_model.php
new file mode 100644
index 00000000..4275713a
--- /dev/null
+++ b/app/modules/billing/models/recurring_model.php
@@ -0,0 +1,885 @@
+CI =& get_instance();
+ $this->CI->load->library('billing/transaction_log');
+ }
+
+ /**
+ * Create a new recurring subscription.
+ *
+ * Creates a new recurring subscription and returns the subscription ID.
+ *
+ * @param int $gateway_id The gateway ID
+ * @param int $customer_id The customer ID
+ * @param date $start_date The date the subscription should begin
+ * @param date $end_date The date the subscription should end
+ * @param date $next_charge_data The date that the subscription should next be charged
+ * @param int $total_occurrences The total number of charges for this subscription.
+ * @param string $notification_url The notification URL
+ * @param int $amount The amount to be charged
+ * @param int $plan_id A link to a plan. Optional.
+ * @param int $card_last_four Last 4 digits of CC (default: 0)
+ * @param int $coupon_id
+ *
+ * @return int The new subscription ID
+ */
+
+ function SaveRecurring($gateway_id, $customer_id, $interval, $start_date, $end_date, $next_charge_date, $total_occurrences, $notification_url, $amount, $plan_id = 0, $card_last_four = 0, $coupon_id = 0)
+ {
+ $timestamp = date('Y-m-d H:i:s');
+ $insert_data = array(
+ 'gateway_id' => $gateway_id,
+ 'customer_id' => $customer_id,
+ 'plan_id' => $plan_id,
+ 'notification_url' => stripslashes($notification_url),
+ 'charge_interval' => $interval,
+ 'start_date' => $start_date,
+ 'end_date' => $end_date,
+ 'next_charge' => $next_charge_date,
+ 'number_occurrences'=> $total_occurrences,
+ 'amount' => $amount,
+ 'card_last_four' => (!empty($card_last_four)) ? $card_last_four : '0',
+ 'active' => '0',
+ 'renewed' => '0',
+ 'updated' => '0',
+ 'expiry_processed' => '0',
+ 'cancel_date' => '0000-00-00 00:00:00',
+ 'timestamp' => $timestamp,
+ 'coupon_id' => $coupon_id
+ );
+
+ $this->db->insert('subscriptions', $insert_data);
+
+ $subscription_id = $this->db->insert_id();
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'subscription_created', $insert_data, __FILE__, __LINE__);
+
+ return $subscription_id;
+ }
+
+ /**
+ * Set Active
+ *
+ * Makes a new subscription active
+ *
+ * @param $subscription_id The recurring ID
+ *
+ * @return boolean TRUE
+ */
+ function SetActive ($recurring_id) {
+ $this->db->update('subscriptions',array('active' => '1'),array('subscription_id' => $recurring_id));
+
+ $this->CI->transaction_log->log_event(FALSE, $recurring_id, 'set_active', FALSE, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Set Renewed
+ *
+ * Sets the "renew" field for a subscription as the subscription ID of the sub that renewed it.
+ *
+ * @param int $old_subscription_id
+ * @param int $new_subscription_id
+ *
+ * @return boolean TRUE
+ */
+ function SetRenew ($old_subscription_id, $new_subscription_id) {
+ $this->db->update('subscriptions',array('renewed' => $new_subscription_id),array('subscription_id' => $old_subscription_id));
+
+ $this->CI->transaction_log->log_event(FALSE, $old_subscription_id, 'mark_as_renewed', array('renewing_subscription' => $new_subscription_id), __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Set Updated
+ *
+ * Sets the "updated" field to the new subscription ID, if the Credit Card was updated
+ *
+ * @param int $old_subscription_id
+ * @param int $new_subscription_id
+ *
+ * @return boolean TRUE
+ */
+ function SetUpdated ($old_subscription_id, $new_subscription_id) {
+ $this->db->update('subscriptions',array('updated' => $new_subscription_id),array('subscription_id' => $old_subscription_id));
+
+ $this->CI->transaction_log->log_event(FALSE, $old_subscription_id, 'mark_as_updated', array('renewing_subscription' => $new_subscription_id), __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Add a customer profile ID.
+ *
+ * For API's that require a customer profile
+ *
+ * @param int $subscription_id The subscription_id
+ * @param int $api_customer_reference The customer profile id
+ *
+ * @return bool TRUE upon success.
+ */
+ function SaveApiCustomerReference($subscription_id, $api_customer_reference)
+ {
+ $update_data = array('api_customer_reference' => $api_customer_reference);
+
+ $this->db->where('subscription_id', $subscription_id);
+ $this->db->update('subscriptions', $update_data);
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'customer_reference_saved', $update_data, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Add a customer payment ID.
+ *
+ * For API's that require a customer payment profile
+ *
+ * @param int $subscription_id The subscription_id
+ * @param int $api_payment_reference The customer payment id
+ *
+ * @return bool TRUE upon success.
+ */
+ function SaveApiPaymentReference($subscription_id, $api_payment_reference)
+ {
+ $update_data = array('api_payment_reference' => $api_payment_reference);
+
+ $this->db->where('subscription_id', $subscription_id);
+ $this->db->update('subscriptions', $update_data);
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'payment_reference_saved', $update_data, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Add a Auth number.
+ *
+ * For API's that require an Auth code be used for future charges
+ *
+ * @param int $subscription_id The subscription_id
+ * @param int $api_auth_number The API auth code.
+ */
+ function SaveApiAuthNumber($subscription_id, $api_auth_number)
+ {
+ $update_data = array('api_auth_number' => $api_auth_number);
+
+ $this->db->where('subscription_id', $subscription_id);
+ $this->db->update('subscriptions', $update_data);
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'auth_number_saved', $update_data, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Make a subscription inactive
+ *
+ * Makes a subscription inactive
+ *
+ * @param int $subscription_id The subscription_id
+ *
+ * @return bool TRUE upon success, FALSE if already inactive.
+ */
+ function MakeInactive($subscription_id)
+ {
+ $subscription = $this->GetSubscriptionDetails($subscription_id);
+
+ $update_data = array('active' => '0', 'cancel_date' => date('Y-m-d H:i:s'));
+
+ // set end date
+ if ($subscription['next_charge'] != '0000-00-00' and (strtotime($subscription['next_charge']) + (60*60*24)) > time() and strtotime($subscription['next_charge']) < strtotime($subscription['end_date'])) {
+ // there's a next charge date which won't be renewed, so we'll end it then
+ // we must also account for their signup time
+ //$time_created = date('H:i:s',strtotime($subscription['date_created']));
+ //$end_date = $subscription['next_charge_date'] . ' ' . $time_created;
+ $end_date = $subscription['next_charge'];
+ }
+ elseif ($subscription['end_date'] != '0000-00-00') {
+ // there is a set end_date
+ $end_date = $subscription['end_date'];
+ }
+ else {
+ // for some reason, neither a next_charge_date or an end_date exist
+ // let's end this now
+ //$end_date = date('Y-m-d H:i:s');
+ $end_date = date('Y-m-d');
+ }
+
+ $update_data['end_date'] = $end_date;
+
+ $this->db->where('subscription_id', $subscription_id);
+ $this->db->update('subscriptions', $update_data);
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'make_inactive', $update_data, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Delete Recurring Completely (Charge Failed)
+ *
+ * @param int $subscription_id
+ */
+
+ function DeleteRecurring ($subscription_id) {
+ $this->db->where('subscription_id',$subscription_id);
+ $this->db->delete('subscriptions');
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'delete_subscription_completely', FALSE, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Add a failure to a subscription
+ *
+ *
+ * @param int $subscription_id The subscription_id
+ *
+ * @return bool TRUE upon success.
+ */
+ function AddFailure($subscription_id, $failures = 0)
+ {
+ $update_data = array('number_charge_failures' => $failures);
+
+ $this->db->where('subscription_id', $subscription_id);
+ $this->db->update('subscriptions', $update_data);
+
+ $this->CI->transaction_log->log_event(FALSE, $subscription_id, 'add_failure', $update_data, __FILE__, __LINE__);
+
+ return TRUE;
+ }
+
+ /**
+ * Get subscription details
+ *
+ * Returns an array of details about the subscription.
+ *
+ * @param int $subscription_id The subscription_id
+ *
+ * @return array Subscription details
+ */
+ function GetSubscriptionDetails($subscription_id)
+ {
+ $this->db->join('gateways', 'gateways.gateway_id = subscriptions.gateway_id', 'inner');
+ $this->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ $this->db->where('subscription_id', $subscription_id);
+ $query = $this->db->get('subscriptions');
+ if($query->num_rows() > 0) {
+ return $query->row_array();
+ } else {
+ die($this->response->Error(5000));
+ }
+ }
+
+ /**
+ * Retrieve details for a specific subscription
+ *
+ * Returns an array of data for the requested subscription.
+ *
+ * @param int $recurring_id The ID # of the recurring transaction to pull;
+ *
+ * @return array|bool Details for a specific subscription or FALSE upon failure
+ */
+
+ function GetRecurring ($recurring_id)
+ {
+ $params = array('id' => $recurring_id);
+
+ $data = $this->GetRecurrings($params, TRUE);
+
+ if (!empty($data)) {
+ return $data[0];
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Search subscriptions.
+ *
+ * Returns an array of results based on submitted search criteria. All fields are optional.
+ *
+ * @param int $params['gateway_id'] The gateway ID used for the order. Optional.
+ * @param date $params['created_after'] Only subscriptions created after or on this date will be returned. Optional.
+ * @param date $params['created_before'] Only subscriptions created before or on this date will be returned. Optional.
+ * @param int $params['customer_id'] The customer id associated with the subscription. Optional.
+ * @param string $params['customer_internal_id'] The customer's internal id associated with the subscription. Optional.
+ * @param int $params['amount'] Only subscriptions for this amount will be returned. Optional.
+ * @param boolean $params['active'] Returns only active subscriptions. Optional.
+ * @param int $params['plan_id'] Only return subscriptions link to this plan ID
+ * @param int $params['offset'] Offsets the database query.
+ * @param int $params['limit'] Limits the number of results returned. Optional.
+ * @param string $params['sort'] Variable used to sort the results. Possible values are date, customer_first_name, customer_last_name, amount. Optional
+ * @param string $params['sort_dir'] Used when a sort param is supplied. Possible values are asc and desc. Optional.
+ *
+ * @return mixed Array containing results
+ */
+ function GetRecurrings ($params, $any_status = FALSE)
+ {
+ // Check which search paramaters are set
+
+ if(isset($params['id'])) {
+ $this->db->where('subscription_id', $params['id']);
+ }
+
+ if(isset($params['gateway_id'])) {
+ $this->db->where('gateway_id', $params['gateway_id']);
+ }
+
+ if(isset($params['created_after'])) {
+ $start_date = date('Y-m-d H:i:s', strtotime($params['created_after']));
+ $this->db->where('timestamp >=', $start_date);
+ }
+
+ if(isset($params['created_before'])) {
+ $end_date = date('Y-m-d H:i:s', strtotime($params['created_before']));
+ $this->db->where('timestamp <=', $end_date);
+ }
+
+ if (isset($params['customer_id'])) {
+ $this->db->where('subscriptions.customer_id', $params['customer_id']);
+ }
+
+ if(isset($params['customer_last_name'])) {
+ $this->db->where('customers.last_name', $params['customer_last_name']);
+ }
+
+ if(isset($params['customer_internal_id'])) {
+ $this->db->where('customers.internal_id', $params['customer_internal_id']);
+ }
+
+ if(isset($params['amount'])) {
+ $this->db->where('amount', $params['amount']);
+ }
+
+ if (isset($params['active'])) {
+ if($params['active'] == '1' or $params['active'] == '0') {
+ $this->db->where('subscriptions.active', $params['active']);
+ }
+ }
+ elseif ($any_status == FALSE) {
+ $this->db->where('subscriptions.active','1');
+ }
+
+ if(isset($params['plan_id'])) {
+ $this->db->where('subscriptions.plan_id', $params['plan_id']);
+ }
+
+ if (isset($params['offset'])) {
+ $offset = $params['offset'];
+ }
+ else {
+ $offset = 0;
+ }
+
+ if(isset($params['limit'])) {
+ $this->db->limit($params['limit'], $offset);
+ }
+
+ if(isset($params['sort_dir']) and ($params['sort_dir'] == 'asc' or $params['sort_dir'] == 'desc' )) {
+ $sort_dir = $params['sort_dir'];
+ }
+ else {
+ $sort_dir = 'desc';
+ }
+
+ $params['sort'] = isset($params['sort']) ? $params['sort'] : '';
+
+ switch ($params['sort'])
+ {
+ case 'date':
+ $sort = 'subscription_id';
+ break;
+ case 'customer_first_name':
+ $sort = 'first_name';
+ break;
+ case 'customer_last_name':
+ $sort = 'last_name';
+ break;
+ case 'amount':
+ $sort = 'amount';
+ break;
+ default:
+ $sort = 'subscription_id';
+ break;
+ }
+
+ $this->db->order_by($sort, $sort_dir);
+
+ $this->db->join('customers', 'customers.customer_id = subscriptions.customer_id', 'left');
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+ $this->db->join('plans', 'plans.plan_id = subscriptions.plan_id', 'left');
+ $this->db->join('plan_types', 'plan_types.plan_type_id = plans.plan_type_id', 'left');
+ $this->db->select('subscriptions.*');
+ $this->db->select('subscriptions.active AS sub_active');
+ $this->db->select('customers.*');
+ $this->db->select('countries.*');
+ $this->db->select('plans.name');
+ $this->db->select('plan_types.type AS plan_type',false);
+ $this->db->select('plans.interval AS plan_interval',false);
+ $this->db->select('plans.amount AS plan_amount',false);
+ $this->db->select('plans.notification_url AS plan_notification_url',false);
+ $query = $this->db->get('subscriptions');
+ $data = array();
+ if($query->num_rows() > 0) {
+ $i=0;
+ foreach($query->result() as $row) {
+ $data[$i]['id'] = $row->subscription_id;
+ $data[$i]['gateway_id'] = $row->gateway_id;
+ $data[$i]['date_created'] = local_time($row->timestamp);
+ $data[$i]['amount'] = money_format("%!^i",$row->amount);
+ $data[$i]['interval'] = $row->charge_interval;
+ $data[$i]['start_date'] = local_time($row->start_date);
+ $data[$i]['end_date'] = local_time($row->end_date);
+ $data[$i]['last_charge_date'] = local_time($row->last_charge);
+ $data[$i]['renewed'] = $row->renewed;
+ $data[$i]['updated'] = $row->updated;
+ $data[$i]['next_charge_date'] = (strtotime($row->next_charge) < strtotime($row->end_date)) ? local_time($row->next_charge) : local_time($row->end_date);
+ if ($row->sub_active == '0' and $row->cancel_date == '0000-00-00 00:00:00') {
+ // this sub never even started
+ $data[$i]['cancel_date'] = local_time($row->start_date);
+ }
+ elseif ($row->sub_active == '0') {
+ $data[$i]['cancel_date'] = local_time($row->cancel_date);
+ }
+ $data[$i]['number_occurrences'] = $row->number_occurrences;
+ $data[$i]['notification_url'] = $row->notification_url;
+ $data[$i]['status'] = ($row->sub_active == '1') ? 'active' : 'inactive';
+ $data[$i]['card_last_four'] = $row->card_last_four;
+
+ if($row->customer_id !== 0) {
+ $data[$i]['customer']['customer_id'] = $row->customer_id;
+ $data[$i]['customer']['id'] = $row->customer_id;
+ $data[$i]['customer']['internal_id'] = $row->internal_id;
+ $data[$i]['customer']['first_name'] = $row->first_name;
+ $data[$i]['customer']['last_name'] = $row->last_name;
+ $data[$i]['customer']['company'] = $row->company;
+ $data[$i]['customer']['address_1'] = $row->address_1;
+ $data[$i]['customer']['address_2'] = $row->address_2;
+ $data[$i]['customer']['city'] = $row->city;
+ $data[$i]['customer']['state'] = $row->state;
+ $data[$i]['customer']['postal_code'] = $row->postal_code;
+ $data[$i]['customer']['country'] = $row->iso2;
+ $data[$i]['customer']['email'] = $row->email;
+ $data[$i]['customer']['phone'] = $row->phone;
+ }
+
+ if($row->plan_id != 0) {
+ $data[$i]['plan']['id'] = $row->plan_id;
+ $data[$i]['plan']['type'] = $row->plan_type;
+ $data[$i]['plan']['name'] = $row->name;
+ $data[$i]['plan']['amount'] = money_format("%!^i",$row->plan_amount);
+ $data[$i]['plan']['interval'] = $row->plan_interval;
+ $data[$i]['plan']['notification_url'] = $row->plan_notification_url;
+ }
+
+ $i++;
+ }
+ } else {
+ return FALSE;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Updates a subscription based on moving it to a new plan
+ *
+ * Upgrades or downgrades a subscription to a new plan
+ *
+ * @param int $recurring_id The ID of the recurring charge
+ * @param int $new_plan_id The ID of the new plan
+ *
+ * @return bool TRUE upon success, FALSE upon failure
+ *
+ */
+ function ChangeRecurringPlan ($recurring_id, $new_plan_id) {
+ $this->CI->load->model('billing/plan_model');
+
+ $plan_details = $this->CI->plan_model->GetPlanDetails($new_plan_id);
+
+ $update = array(
+ 'plan_id' => $plan_details->plan_id,
+ 'amount' => $plan_details->amount,
+ 'recur' => array('interval' => $plan_details->interval),
+ 'notification_url' => $plan_details->notification_url,
+ 'recurring_id' => $recurring_id
+ );
+
+ $this->CI->transaction_log->log_event(FALSE, $recurring_id, 'change_recurring_plan', $update, __FILE__, __LINE__);
+
+ return $this->UpdateRecurring($update);
+ }
+
+ /**
+ * Update an existing subscription.
+ *
+ * Updates an existing subscription with new parameters.
+ *
+ * @param int $params['recurring_id'] The subscription ID to update.
+ * @param string $params['notification_url'] The new notification URL. Optional.
+ * @param int $params['customer_id'] The new customer id. Optional.
+ * @param int $params['amount'] The new amount to charge. Optional
+ * @param int $params['interval'] The new number of days between charges. Optional.
+ * @param int $params['plan_id'] The new plan ID. Optional.
+ *
+ * @return bool TRUE upon success, FALSE upon failure
+ *
+ */
+ function UpdateRecurring($params)
+ {
+ if(!isset($params['recurring_id'])) {
+ return FALSE;
+ }
+
+ if(isset($params['notification_url'])) {
+ $update_data['notification_url'] = $params['notification_url'];
+ }
+
+ if(isset($params['customer_id'])) {
+ $update_data['customer_id'] = $params['customer_id'];
+ $this->CI->load->model('billing/customer_model');
+ $customer = $this->CI->customer_model->GetCustomerDetails($params['customer_id']);
+ } else {
+ $customer = FALSE;
+ }
+
+ if(isset($params['amount'])) {
+ $update_data['amount'] = $params['amount'];
+ }
+
+ if(isset($params['plan_id'])) {
+ $update_data['plan_id'] = $params['plan_id'];
+ }
+
+ if(isset($params['next_charge_date'])) {
+ $this->load->library('field_validation');
+ if ($this->field_validation->ValidateDate($params['next_charge_date'])) {
+ $update_data['next_charge'] = $params['next_charge_date'];
+ }
+ else {
+ die($this->response->Error(5007));
+ }
+ }
+
+ $subscription = $this->GetSubscriptionDetails($params['recurring_id']);
+ //print_r($subscription);
+ if(isset($params['recur']['interval'])) {
+ $update_data['charge_interval'] = $params['recur']['interval'];
+ // Get the subcription details
+
+ $start_date = $subscription['start_date'];
+ $end_date = $subscription['end_date'];
+ // Figure the total number of occurrences
+ $update_data['number_occurrences'] = round((strtotime($end_date) - strtotime($start_date)) / ($params['recur']['interval'] * 86400), 0);
+ }
+
+ if(!isset($update_data)) {
+ die($this->response->Error(6003));
+ }
+
+ // Make sure they update their own subscriptions
+ $this->db->where('subscription_id', $params['recurring_id']);
+
+ $this->db->update('subscriptions', $update_data);
+
+ $this->CI->transaction_log->log_event(FALSE, $params['recurring_id'], 'update_recurring', $update_data, __FILE__, __LINE__);
+
+ // Update the subscription with the gateway
+ $this->CI->load->model('billing/gateway_model');
+
+ $gateway = $this->CI->gateway_model->GetGatewayDetails($subscription['gateway_id']);
+ $gateway_type = $gateway['name'];
+
+ $this->load->library('billing/payment/'.$gateway_type);
+
+ // get the settings for the gateway
+ $settings = $this->$gateway_type->Settings();
+
+ if($settings['allows_updates'] === 0) {
+ die($this->response->Error(5016));
+ }
+
+ $update_success = $this->$gateway_type->UpdateRecurring($gateway, $subscription, $customer, $params);
+
+ $this->CI->transaction_log->log_event(FALSE, $params['recurring_id'], 'update_recurring_gateway', array('result' => $update_success), __FILE__, __LINE__);
+
+ if (!$update_success) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ /*
+ * Cancels the recurring billing
+ *
+ * @param int $recurring_id The recurring charge ID
+ *
+ * @return bool TRUE upon success, FALSE upon fail
+ *
+ */
+
+ function CancelRecurring($recurring_id, $expiring = FALSE)
+ {
+ $this->CI->transaction_log->log_event(FALSE, $recurring_id, 'cancel_requested', array('expiring' => $expiring), __FILE__, __LINE__);
+
+ // Get the subscription information
+ $subscription = $this->GetSubscriptionDetails($recurring_id);
+
+ // Get the gateway info to load the proper library
+ $this->CI->load->model('billing/gateway_model');
+ $gateway = $this->CI->gateway_model->GetGatewayDetails($subscription['gateway_id']);
+
+ $gateway_name = $subscription['name'];
+ $this->load->library('billing/payment/'.$gateway_name);
+ $cancelled = $this->$gateway_name->CancelRecurring($subscription, $gateway);
+
+ $this->CI->transaction_log->log_event(FALSE, $recurring_id, 'cancel_gateway', array('result' => $cancelled), __FILE__, __LINE__);
+
+ $this->MakeInactive($recurring_id);
+
+ if ($cancelled) {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+ }
+
+ function GetPlansByCustomer($customer_id)
+ {
+ $this->db->join('plans', 'plans.plan_id = subscriptions.plan_id', 'inner');
+ $this->db->join('plan_types', 'plan_types.plan_type_id = plans.plan_type_id', 'inner');
+ $this->db->where('customer_id', $customer_id);
+ $this->db->where('subscriptions.plan_id <>', 0);
+ $query = $this->db->get('subscriptions');
+ if($query->num_rows() > 0) {
+ return $query->result();
+ } else {
+ return FALSE;
+ }
+ }
+
+ function GetAllSubscriptionsByGatewayID($gateway_id)
+ {
+ $this->db->join('gateways', 'subscriptions.gateway_id = gateways.gateway_id', 'inner');
+ $this->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ $this->db->where('subscriptions.gateway_id', $gateway_id);
+ $this->db->where('subscriptions.active', 1);
+ $query = $this->db->get('subscriptions');
+
+ if($query->num_rows > 0) {
+ return $query->result_array();
+ } else {
+ return FALSE;
+ }
+
+ }
+
+ function GetAllSubscriptionsByChargeDate($date = FALSE)
+ {
+ if(!$date) {
+ $date = date('Y-m-d');
+ }
+
+ $this->db->join('gateways', 'subscriptions.gateway_id = gateways.gateway_id', 'inner');
+ $this->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ $this->db->where('next_charge', $date);
+ $this->db->where('subscriptions.active', 1);
+ $query = $this->db->get('subscriptions');
+
+ if($query->num_rows > 0) {
+ return $query->result_array();
+ } else {
+ return FALSE;
+ }
+
+ }
+
+ function GetAllSubscriptionsForExpiring() {
+ $this->db->join('gateways', 'subscriptions.gateway_id = gateways.gateway_id', 'inner');
+ $this->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ $this->db->where('end_date <= NOW()');
+ $this->db->where('subscriptions.active', 1);
+ $query = $this->db->get('subscriptions');
+
+ if($query->num_rows > 0) {
+ return $query->result_array();
+ } else {
+ return FALSE;
+ }
+ }
+
+ function GetAllSubscriptionsForCharging($date = FALSE)
+ {
+ if (!$date) {
+ $date = date('Y-m-d');
+ }
+
+ $this->db->join('gateways', 'subscriptions.gateway_id = gateways.gateway_id', 'inner');
+ $this->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ $this->db->where('next_charge <=', $date);
+ $this->db->where('subscriptions.active', '1');
+ $query = $this->db->get('subscriptions');
+
+ if ($query->num_rows > 0) {
+ return $query->result_array();
+ } else {
+ return FALSE;
+ }
+ }
+
+ function GetAllSubscriptionsByDate($date_type = FALSE, $date = FALSE)
+ {
+ if(!$date) {
+ $date = date('Y-m-d');
+ }
+
+ if(!$date_type) {
+ $date_type = 'next_charge';
+ }
+
+ $this->db->join('gateways', 'subscriptions.gateway_id = gateways.gateway_id', 'inner');
+ $this->db->join('external_apis', 'gateways.external_api_id = external_apis.external_api_id', 'inner');
+ $this->db->where($date_type, $date);
+ $this->db->where('subscriptions.active', 1);
+ $query = $this->db->get('subscriptions');
+
+ if($query->num_rows > 0) {
+ return $query->result_array();
+ } else {
+ return FALSE;
+ }
+ }
+
+ function GetNextChargeDate($subscription_id, $from_date = FALSE)
+ {
+ if(!$from_date) {
+ $from_date = date('Y-m-d');
+ }
+
+ $this->db->where('subscription_id', $subscription_id);
+ $query = $this->db->get('subscriptions');
+ if($query->num_rows() > 0) {
+ $row = $query->row();
+
+ $from_date = (empty($from_date)) ? $row->next_charge : $from_date;
+
+ $next_charge = strtotime($from_date) + ($row->charge_interval * 86400);
+ return date('Y-m-d', $next_charge);
+ }
+
+ return FALSE;
+ }
+
+ function SetChargeDates($subscription_id, $last_charge, $next_charge)
+ {
+ $update_data = array('last_charge' => $last_charge, 'next_charge' => $next_charge);
+
+ $this->db->where('subscription_id', $subscription_id);
+ $this->db->update('subscriptions', $update_data);
+ }
+
+ /**
+ * Get Details of the last order for a customer.
+ *
+ * Returns array of order details for a specific order_id.
+ *
+ * @param int $customer_id The customer ID.
+ *
+ * @return array|bool Array with charge details or FALSE upon failure
+ */
+
+ function GetChargesByDate($date)
+ {
+ $date = date('Y-m-d', $date);
+
+ $this->db->join('customers', 'customers.customer_id = subscriptions.customer_id', 'left');
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+ $this->db->where('subscriptions.active', 1);
+ $this->db->where('next_charge', $date);
+ $this->db->where('end_date >', $date);
+ $query = $this->db->get('subscriptions');
+ if($query->num_rows() > 0) {
+ return $query->result_array();
+ } else {
+ return FALSE;
+ }
+ }
+
+
+ /**
+ * Get Details of the last order for a customer.
+ *
+ * Returns array of order details for a specific order_id.
+ *
+ * @param int $customer_id The customer ID.
+ *
+ * @return array|bool Array with charge details or FALSE upon failure
+ */
+
+ function GetChargesByExpiryDate($date)
+ {
+ $this->db->join('customers', 'customers.customer_id = subscriptions.customer_id', 'left');
+ $this->db->join('countries', 'countries.country_id = customers.country', 'left');
+ $this->db->where('subscriptions.active', 1);
+ $this->db->where('end_date', date('Y-m-d', $date));
+ $query = $this->db->get('subscriptions');
+ if($query->num_rows() > 0) {
+ return $query->result_array();
+ } else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Get Details of the last order for a customer.
+ *
+ * Returns array of order details for a specific order_id.
+ *
+ * @param int $customer_id The customer ID.
+ *
+ * @return array|bool Array with charge details or FALSE upon failure
+ */
+
+ function CancelRecurringByGateway($gateway_id)
+ {
+ $this->db->select('subscription_id');
+ $this->db->where('gateway_id', $gateway_id);
+ $query = $this->db->get('subscriptions');
+ if($query->num_rows() > 0) {
+ foreach($query->result() as $row) {
+ $this->CancelRecurring($row->subscription_id);
+ }
+
+ return TRUE;
+
+ } else {
+ return FALSE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/subscription_model.php b/app/modules/billing/models/subscription_model.php
new file mode 100644
index 00000000..dd9ef0c5
--- /dev/null
+++ b/app/modules/billing/models/subscription_model.php
@@ -0,0 +1,754 @@
+load->model('store/taxes_model');
+
+ if ($tax = $CI->taxes_model->get_tax_for_subscription($subscription_id)) {
+ $CI->taxes_model->record_tax($tax['tax_id'], $charge_id, 0, $tax['tax_amount']);
+ }
+ }
+
+ /**
+ * Hook: Subscription New
+ *
+ * Called by the subscription_new hook
+ *
+ * @param int $subscription_id
+ */
+ function hook_subscription_new ($subscription_id) {
+ $subscription = $this->get_subscription($subscription_id);
+
+ $CI =& get_instance();
+ $CI->load->model('billing/subscription_plan_model');
+
+ $plan = $CI->subscription_plan_model->get_plan($subscription['plan_id']);
+
+ if (!empty($plan['promotion'])) {
+ $CI->user_model->add_group($subscription['user_id'], $plan['promotion']);
+ }
+
+ // mark the subscription as setup
+ $CI->db->update('subscriptions', array('completed' => '1'), array('subscription_id' => $subscription_id));
+
+ // any renewal maintenance?
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get('r' . $subscription_id);
+
+ if (isset($data['mark_as_renewed'])) {
+ $CI->load->model('billing/recurring_model');
+ $CI->recurring_model->SetRenew($data['mark_as_renewed'], $subscription_id);
+ $CI->recurring_model->CancelRecurring($data['mark_as_renewed']);
+ }
+ }
+
+ /**
+ * Hook: Subscription Renew
+ *
+ * Called by the subscription_renew hook
+ *
+ * @param int $subscription_id
+ */
+ function hook_subscription_renew ($subscription_id) {
+ $subscription = $this->get_subscription($subscription_id);
+
+ $CI =& get_instance();
+ $CI->load->model('billing/subscription_plan_model');
+
+ $plan = $CI->subscription_plan_model->get_plan($subscription['plan_id']);
+
+ if (!empty($plan['promotion'])) {
+ $CI->user_model->add_group($subscription['user_id'], $plan['promotion']);
+ }
+
+ // mark the subscription as setup
+ $CI->db->update('subscriptions', array('completed' => '1'), array('subscription_id' => $subscription_id));
+
+ // any renewal maintenance?
+ $CI->load->model('billing/charge_data_model');
+ $data = $CI->charge_data_model->Get('r' . $subscription_id);
+
+ if (isset($data['mark_as_renewed'])) {
+ $CI->load->model('billing/recurring_model');
+ $CI->recurring_model->SetRenew($data['mark_as_renewed'], $subscription_id);
+ $CI->recurring_model->CancelRecurring($data['mark_as_renewed']);
+ }
+ }
+
+ /**
+ * Hook: Subscription Expire
+ *
+ * Called by the subscription_expire hook
+ *
+ * @param int $subscription_id
+ */
+ function hook_subscription_expire ($subscription_id) {
+ $subscription = $this->get_subscription($subscription_id);
+
+ $CI =& get_instance();
+ $CI->load->model('billing/subscription_plan_model');
+
+ $plan = $CI->subscription_plan_model->get_plan($subscription['plan_id']);
+
+ if (!empty($plan['demotion'])) {
+ $CI->user_model->add_group($subscription['user_id'], $plan['demotion']);
+ }
+
+ // remove from promotion?
+ if (!empty($plan['promotion'])) {
+ $remove_from_group = TRUE;
+
+ // if there is an active group that promotes to this group, however, we won't remove them...
+ $subscriptions = $this->get_subscriptions(
+ array(
+ 'user_id' => $subscription['user_id'],
+ 'end_date_after' => date('Y-m-d',strtotime('now + 2 days')),
+ 'promotion' => $plan['promotion']
+ )
+ );
+
+ if (!empty($subscriptions)) {
+ $remove_from_group = FALSE;
+ }
+
+ if ($remove_from_group == TRUE) {
+ $CI->user_model->remove_group($subscription['user_id'], $plan['promotion']);
+ }
+ }
+ }
+
+ /**
+ * Hook: Member Delete
+ *
+ * Called by the member_delete hook
+ *
+ * @param int $member_id
+ */
+ function hook_member_delete ($member_id) {
+ $subscriptions = $this->get_subscriptions_friendly(array('active' => TRUE), $member_id);
+
+ if (!empty($subscriptions)) {
+ foreach ($subscriptions as $subscription) {
+ $this->cancel_subscription($subscription['id']);
+ }
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Cancel Subscription
+ *
+ * @param int $subscription_id
+ *
+ * @return boolean
+ */
+ public function cancel_subscription ($subscription_id) {
+ $CI =& get_instance();
+ $CI->load->model('billing/recurring_model');
+ $return = $CI->recurring_model->CancelRecurring($subscription_id);
+
+ if ($return == TRUE) {
+ // hook
+ $this->app_hooks->data('subscription',$subscription_id);
+ $this->app_hooks->trigger('subscription_cancel');
+ $this->app_hooks->reset();
+ }
+
+ return $return;
+ }
+
+ /**
+ * Expire Subscription
+ *
+ * @param int $subscription_id
+ *
+ * @return boolean
+ */
+ public function expire_subscription ($subscription_id) {
+ $this->db->update('subscriptions', array('expiry_processed' => '1'), array('subscription_id' => $subscription_id));
+
+ return TRUE;
+ }
+
+ /**
+ * Member Has Subscriptions?
+ *
+ * @param int $member_id (default: active user)
+ *
+ * @return boolean
+ */
+ function has_subscriptions ($member_id = FALSE) {
+ if ($member_id === FALSE) {
+ $CI =& get_instance();
+ if ($CI->user_model->logged_in()) {
+ $member_id = $CI->user_model->get('id');
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ if ($this->get_subscriptions(array('user_id' => $member_id, 'end_date_after' => date('Y-m-d H:i:s'))) !== FALSE) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Get Log
+ *
+ * Get the transaction log for a given subscription
+ *
+ * @param int $subscription_id
+ *
+ * @return array
+ */
+ function get_log ($filters) {
+ $result = $this->db->where('subscription_id', $filters['id'])
+ ->get('transaction_log');
+
+ $log = array();
+ foreach ($result->result_array() as $logged) {
+ $log[] = array(
+ 'ip' => $logged['log_ip'],
+ 'date' => $logged['log_date'],
+ 'browser' => $logged['log_browser'],
+ 'event' => $logged['log_event'],
+ 'data' => unserialize($logged['log_data']),
+ 'file' => $logged['log_file'],
+ 'line' => $logged['log_line']
+ );
+ }
+
+ return $log;
+ }
+
+ /**
+ * Get Subscriptions Friendly
+ *
+ * A friendly wrapper for $this->get_subscriptions();
+ *
+ * @param boolean $filters['active'] Is the subscription still active on the account (i.e., end_date < now)?
+ * @param boolean $filters['recurring'] Is the subscription still actively recurring?
+ * @param int $filters['id'] The subscription ID
+ * @param int $filters['plan_id'] The subscription plan ID
+ * @param int $member_id The member ID to tie the subscription to (default: active user).
+ *
+ * @return array subscriptions
+ */
+ public function get_subscriptions_friendly ($filters = array(), $member_id = FALSE) {
+ if ($member_id === FALSE) {
+ $CI =& get_instance();
+ if ($CI->user_model->logged_in()) {
+ $member_id = $CI->user_model->get('id');
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ // prep filters for the next call
+ $new_filters = array();
+
+ $new_filters['user_id'] = $member_id;
+
+ if (isset($filters['plan_id'])) {
+ // `subscription_plans` table
+ $new_filters['plan_id'] = $filters['plan_id'];
+ }
+
+ if (isset($filters['id'])) {
+ $new_filters['id'] = $filters['id'];
+ }
+
+ if (isset($filters['active'])) {
+ $new_filters['end_date_after'] = date('Y-m-d H:i:s');
+ }
+
+ if (isset($filters['recurring'])) {
+ $new_filters['active'] = '1';
+ }
+
+ $subscriptions = $this->get_subscriptions($new_filters);
+
+ return $subscriptions;
+ }
+
+ /**
+ * Get Subscription
+ *
+ * @param int $subscription_id
+ *
+ * @return array Subscription details, else FALSE
+ */
+ public function get_subscription ($subscription_id) {
+ if (isset($this->cache[$subscription_id])) {
+ return $this->cache[$subscription_id];
+ }
+
+ $sub = $this->get_subscriptions(array('id' => $subscription_id));
+
+ if (empty($sub)) {
+ return FALSE;
+ }
+
+ $this->cache[$subscription_id] = $sub[0];
+ return $sub[0];
+ }
+
+ /**
+ * Count Subscriptions
+ */
+ function count_subscriptions ($filters) {
+ return $this->get_subscriptions($filters, TRUE);
+ }
+
+ /**
+ * Get Subscriptions
+ *
+ * Returns an array of results based on submitted search criteria.
+ *
+ * @param int $filters['id'] The subscription ID
+ * @param string $filters['status'] One of "recurring", "will_expire", "expired", "renewed", "updated"
+ * @param int $filters['gateway_id'] The gateway ID used for the order. Optional.
+ * @param date $filters['created_after'] Only subscriptions created after or on this date will be returned. Optional.
+ * @param date $filters['created_before'] Only subscriptions created before or on this date will be returned. Optional.
+ * @param date $filters['end_date_after'] Only subscriptions ending after or on this date will be returned. Optional.
+ * @param date $filters['end_date_before'] Only subscriptions ending before or on this date will be returned. Optional.
+ * @param int $filters['user_id'] The customer id associated with the subscription. Optional.
+ * @param int $filters['amount'] Only subscriptions for this amount will be returned. Optional.
+ * @param boolean $filters['active'] Returns only active subscriptions. Optional.
+ * @param int $filters['plan_id'] Only return subscriptions link to this subscription_plan_id. Optional.
+ * @param int $filters['offset'] Offsets the database query.
+ * @param int $filters['limit'] Limits the number of results returned. Optional.
+ * @param string $filters['sort'] Variable used to sort the results. Possible values are date, customer_first_name, customer_last_name, amount. Optional
+ * @param string $filters['sort_dir'] Used when a sort param is supplied. Possible values are asc and desc. Optional.
+ *
+ * @return mixed Array containing results
+ */
+ public function get_subscriptions ($filters, $counting = FALSE)
+ {
+ if (isset($filters['id'])) {
+ $this->db->where('subscription_id', $filters['id']);
+ }
+
+ if(isset($filters['gateway_id'])) {
+ $this->db->where('gateway_id', $filters['gateway_id']);
+ }
+
+ if (isset($filters['created_after'])) {
+ $start_date = date('Y-m-d', strtotime($filters['created_after']));
+ $this->db->where('timestamp >=', $start_date);
+ }
+
+ if (isset($filters['created_before'])) {
+ $end_date = date('Y-m-d', strtotime($filters['created_before']));
+ $this->db->where('timestamp <=', $end_date);
+ }
+
+ if (isset($filters['end_date_after'])) {
+ $end_date = date('Y-m-d H:i:s', strtotime($filters['end_date_after']));
+ $this->db->where('end_date >=', $end_date);
+ $this->db->where('end_date !=','0000-00-00');
+ }
+
+ if (isset($filters['end_date_before'])) {
+ $end_date = date('Y-m-d H:i:s', strtotime($filters['end_date_before']));
+ $this->db->where('end_date <=', $end_date);
+ $this->db->where('end_date !=','0000-00-00');
+ }
+
+ if (isset($filters['cancel_date_after'])) {
+ $end_date = date('Y-m-d H:i:s', strtotime($filters['cancel_date_after']));
+ $this->db->where('cancel_date >=', $end_date);
+ $this->db->where('cancel_date !=','0000-00-00');
+ }
+
+ if (isset($filters['cancel_date_before'])) {
+ $end_date = date('Y-m-d H:i:s', strtotime($filters['cancel_date_before']));
+ $this->db->where('cancel_date <=', $end_date);
+ $this->db->where('cancel_date !=','0000-00-00');
+ }
+
+ if (isset($filters['user_id'])) {
+ $this->db->where('users.user_id', $filters['user_id']);
+
+ // if we are loading subs for a user, we assume we only want subs that are "completed" (i.e., fully set up)
+ $this->db->where('completed','1');
+ }
+
+ if (isset($filters['member_name'])) {
+ if (is_numeric($filters['member_name'])) {
+ // we are passed a member id
+ $this->db->where('users.user_id',$filters['member_name']);
+ } else {
+ $this->db->like('users.user_last_name', $filters['member_name']);
+ }
+ }
+
+ if(isset($filters['amount'])) {
+ $this->db->where('subscriptions.amount', $filters['amount']);
+ }
+
+ if (isset($filters['active'])) {
+ $this->db->where('subscriptions.active', $filters['active']);
+ }
+
+ if (isset($filters['plan_id'])) {
+ $this->db->where('subscription_plans.subscription_plan_id', $filters['plan_id']);
+ }
+
+ if (isset($filters['promotion'])) {
+ $this->db->where('subscription_plans.subscription_plan_promotion', $filters['promotion']);
+ }
+
+ if (isset($filters['expiry_processed'])) {
+ $this->db->where('subscriptions.expiry_processed', $filters['expiry_processed']);
+ }
+
+ if (isset($filters['status'])) {
+ if ($filters['status'] == 'recurring') {
+ $this->db->where('subscriptions.active','1');
+ $this->db->where('subscriptions.end_date >',date('Y-m-d'));
+ }
+ elseif ($filters['status'] == 'will_expire') {
+ $this->db->where('subscriptions.active','0');
+ $this->db->where('subscriptions.end_date >',date('Y-m-d'));
+ }
+ elseif ($filters['status'] == 'expired') {
+ $this->db->where('subscriptions.end_date <',date('Y-m-d'));
+ $this->db->where('subscriptions.renewed','0');
+ $this->db->where('subscriptions.updated','0');
+ }
+ elseif ($filters['status'] == 'renewed') {
+ $this->db->where('subscriptions.renewed !=','0');
+ }
+ elseif ($filters['status'] == 'updated') {
+ $this->db->where('subscriptions.updated !=','0');
+ }
+ }
+
+ // standard ordering and limiting
+ $order_by = (isset($filters['sort'])) ? $filters['sort'] : 'subscriptions.subscription_id';
+ $order_dir = (isset($filters['sort_dir'])) ? $filters['sort_dir'] : 'DESC';
+ $this->db->order_by($order_by, $order_dir);
+
+ if (isset($filters['limit'])) {
+ $offset = (isset($filters['offset'])) ? $filters['offset'] : 0;
+ $this->db->limit($filters['limit'], $offset);
+ }
+
+ $this->db->join('customers', 'customers.customer_id = subscriptions.customer_id', 'inner');
+ $this->db->join('users', 'users.user_id = customers.internal_id','inner');
+ $this->db->join('plans', 'plans.plan_id = subscriptions.plan_id', 'inner');
+ $this->db->join('subscription_plans','subscription_plans.plan_id = plans.plan_id');
+ $this->db->join('plan_types', 'plan_types.plan_type_id = plans.plan_type_id', 'inner');
+
+ if ($counting == FALSE) {
+ $this->db->select('subscriptions.*');
+ $this->db->select('subscriptions.active AS sub_active');
+ $this->db->select('subscription_plans.subscription_plan_id');
+ $this->db->select('users.*');
+ $this->db->select('plans.name');
+ $this->db->select('plan_types.type AS plan_type',false);
+ $this->db->select('plans.interval AS plan_interval',false);
+ $this->db->select('plans.amount AS plan_amount',false);
+
+ $result = $this->db->get('subscriptions');
+ }
+ else {
+ $this->db->select('COUNT(`subscriptions`.`subscription_id`) AS `counted`',FALSE);
+ $result = $this->db->get('subscriptions');
+ $count = $result->row_array();
+ return $count['counted'];
+ }
+
+ if ($result->num_rows() == 0) {
+ return FALSE;
+ }
+
+ $subscriptions = array();
+
+ foreach($result->result_array() as $subscription) {
+ $this_subscription = array(
+ 'id' => $subscription['subscription_id'],
+ 'user_id' => $subscription['user_id'],
+ 'user_username' => $subscription['user_username'],
+ 'user_first_name' => $subscription['user_first_name'],
+ 'user_last_name' => $subscription['user_last_name'],
+ 'user_email' => $subscription['user_email'],
+ 'gateway_id' => $subscription['gateway_id'],
+ 'date_created' => local_time($subscription['timestamp']),
+ 'amount' => money_format("%!^i",$subscription['amount']),
+ 'interval' => $subscription['charge_interval'],
+ 'start_date' => local_time($subscription['start_date']),
+ 'end_date' => ($subscription['end_date'] != '0000-00-00') ? local_time($subscription['end_date']) : FALSE,
+ 'last_charge_date' => ($subscription['last_charge'] != '0000-00-00') ? local_time($subscription['last_charge']) : FALSE,
+ 'next_charge_date' => (strtotime($subscription['next_charge']) < strtotime($subscription['end_date']) and $subscription['next_charge'] != '0000-00-00') ? local_time($subscription['next_charge']) : FALSE,
+ 'cancel_date' => ($subscription['cancel_date'] != '0000-00-00') ? local_time($subscription['cancel_date']) : FALSE,
+ 'number_occurrences' => $subscription['number_occurrences'],
+ 'active' => ($subscription['sub_active'] == '1') ? TRUE : FALSE,
+ 'coupon_id' => $subscription['coupon_id'],
+ 'renewing_subscription_id' => $subscription['renewed'],
+ 'updating_subscription_id' => $subscription['updated'],
+ 'card_last_four' => (!empty($subscription['card_last_four'])) ? $subscription['card_last_four'] : FALSE,
+ 'plan_id' => $subscription['subscription_plan_id'], // for `subscription_plans`
+ 'renew_link' => site_url('billing/subscriptions/renew/' . $subscription['subscription_id']),
+ 'cancel_link' => site_url('users/cancel/' . $subscription['subscription_id']),
+ 'update_cc_link' => (!empty($subscription['card_last_four'])) ? site_url('users/update_cc/' . $subscription['subscription_id']) : FALSE,
+ // status fields for this abstraction
+ 'is_recurring' => ($subscription['sub_active'] == '1' and (strtotime($subscription['end_date']) > strtotime($subscription['next_charge']))) ? TRUE : FALSE,
+ 'is_active' => (strpos($subscription['end_date'],'0000-00-00') === TRUE or strtotime($subscription['end_date']) > time()) ? TRUE : FALSE,
+ 'is_renewed' => empty($subscription['renewed']) ? FALSE : TRUE,
+ 'is_updated' => empty($subscription['updated']) ? FALSE : TRUE
+ );
+
+ if ($subscription['sub_active'] == '0' and $subscription['cancel_date'] == '0000-00-00 00:00:00') {
+ // this sub never even started
+ $this_subscription['cancel_date'] = local_time($subscription['start_date']);
+ }
+ elseif (empty($subscription['sub_active'])) {
+ $this_subscription['cancel_date'] = local_time($subscription['cancel_date']);
+ }
+
+ if (!empty($subscription['plan_id'])) {
+ $this_subscription['plan']['id'] = $subscription['plan_id'];
+ $this_subscription['plan']['type'] = $subscription['plan_type'];
+ $this_subscription['plan']['name'] = $subscription['name'];
+ $this_subscription['plan']['amount'] = money_format("%!^i",$subscription['plan_amount']);
+ $this_subscription['plan']['interval'] = $subscription['plan_interval'];
+ }
+
+ $subscriptions[] = $this_subscription;
+ }
+
+ return $subscriptions;
+ }
+
+ function hook_cron () {
+ $CI =& get_instance();
+
+ cron_log('Beginning billing cronjob.');
+
+ $run_cron = TRUE;
+
+ // cron run time?
+ if ($CI->config->item('billing_cron_time')) {
+ $run_time = (int)$CI->config->item('billing_cron_time');
+ }
+ else {
+ $run_time = 11;
+ }
+
+ // we only need this to run once per day
+ if (setting('cron_billing_last_update') === FALSE) {
+ cron_log('Created billing last update setting to track billing cron runs and limit them to once per day.');
+
+ $this->settings_model->new_setting(1, 'cron_billing_last_update', date('Y-m-d H:i:s'), 'When did the billing cron job last run?', 'text', '', FALSE, TRUE);
+ }
+ elseif (date('Y-m-d') == date('Y-m-d', strtotime(setting('cron_billing_last_update')))) {
+ cron_log('Billing cronjob has already run today. Exiting.');
+
+ $run_cron = FALSE;
+ }
+ elseif ((int)date('H') < $run_time) {
+ cron_log('Billing cronjob is configured not to run until ' . $run_time . ' hours. Exiting.');
+
+ $run_cron = FALSE;
+ }
+ else {
+ cron_log('Updated billing last update setting to current time. We\'re running it!');
+
+ $this->settings_model->update_setting('cron_billing_last_update', date('Y-m-d H:i:s'));
+ }
+
+ if ($run_cron == FALSE) {
+ return;
+ }
+
+ // subscription maintenance
+ $CI->load->model('billing/gateway_model');
+ $CI->load->model('billing/recurring_model');
+
+ // cancel subscriptions if end_date is today or earlier and they are still active
+ $cancelled = array();
+ $subscriptions = $this->get_subscriptions(array(
+ 'end_date_before' => date('Y-m-d',strtotime('now')),
+ 'active' => '1'
+ ));
+ if (!empty($subscriptions)) {
+ foreach($subscriptions as $subscription) {
+ $response = $this->cancel_subscription($subscription['id']);
+ if ($response) {
+ $this->app_hooks->data('subscription',$subscription['id']);
+ $this->app_hooks->trigger('subscription_cancel', $subscription['id']);
+ $this->app_hooks->reset();
+ $cancelled[] = $subscription['id'];
+ }
+ }
+ }
+
+ // expire subscriptions with an end_date of today or earlier and expiry_processed == 0
+ $expired = array();
+ $subscriptions = $this->get_subscriptions(array(
+ 'end_date_before' => date('Y-m-d',strtotime('now')),
+ 'expiry_processed' => '0'
+ ));
+ if (!empty($subscriptions)) {
+ foreach($subscriptions as $subscription) {
+ $this->expire_subscription($subscription['id']);
+
+ if ($subscription['is_renewed'] == TRUE or $subscription['is_updated'] == TRUE) {
+ // don't send confusing notices
+ continue;
+ }
+
+ $this->app_hooks->data('subscription',$subscription['id']);
+ $this->app_hooks->trigger('subscription_expire', $subscription['id']);
+ $this->app_hooks->reset();
+ $expired[] = $subscription['id'];
+ }
+ }
+
+ // charge subscriptions that need to be charged today
+ $today = date('Y-m-d');
+ $subscriptions = $CI->recurring_model->GetAllSubscriptionsForCharging($today);
+
+ $charge_success = array();
+ $charge_failure = array();
+ if ($subscriptions) {
+ foreach ($subscriptions as $subscription) {
+ // try and make the charge
+ $response = $CI->gateway_model->ChargeRecurring($subscription);
+ if($response) {
+ $charge_success[] = $subscription['subscription_id'];
+ } else {
+ $charge_failure[] = $subscription['subscription_id'];
+ }
+ }
+ }
+
+ // Check for emails to send
+ // Get all the recurring charge emails to send in one week
+ $sent_emails['subscription_autorecur_in_week'] = array();
+ $next_week = mktime(0,0,0, date('m'), date('d') + 7, date('Y'));
+ $charges = $CI->recurring_model->GetChargesByDate($next_week);
+ if ($charges) {
+ foreach($charges as $charge) {
+ $this->app_hooks->data('subscription',$charge['subscription_id']);
+ $this->app_hooks->trigger('subscription_renew_1_week', $charge['subscription_id']);
+ $this->app_hooks->reset();
+
+ $sent_emails['subscription_autorecur_in_week'][] = $charge['subscription_id'];
+ }
+ }
+
+ // Get all the recurring charge emails to send in one month
+ $sent_emails['subscription_autorecur_in_month'] = array();
+ $next_month = mktime(0,0,0, date('m') + 1, date('d'), date('Y'));
+ $charges = $CI->recurring_model->GetChargesByDate($next_month);
+ if ($charges) {
+ foreach($charges as $charge) {
+ if ($charge['renewed'] == '1' or $charge['updated'] == '1') {
+ // don't send confusing notices
+ continue;
+ }
+
+ $this->app_hooks->data('subscription',$charge['subscription_id']);
+ $this->app_hooks->trigger('subscription_renew_1_month', $charge['subscription_id']);
+ $this->app_hooks->reset();
+
+ $sent_emails['subscription_autorecur_in_month'][] = $charge['subscription_id'];
+ }
+ }
+
+ // Get all the recurring expiration emails to send in one week
+ $sent_emails['subscription_expiring_in_week'] = array();
+ $charges = $CI->recurring_model->GetChargesByExpiryDate($next_week);
+ if ($charges) {
+ foreach($charges as $charge) {
+ if ($charge['renewed'] == '1' or $charge['updated'] == '1') {
+ // don't send confusing notices
+ continue;
+ }
+
+ if (empty($charge['renewed']) and empty($charge['updated'])) {
+ $this->app_hooks->data('subscription',$charge['subscription_id']);
+ $this->app_hooks->trigger('subscription_expire_1_week', $charge['subscription_id']);
+ $this->app_hooks->reset();
+
+ $sent_emails['subscription_expiring_in_week'][] = $charge['subscription_id'];
+ }
+ }
+ }
+
+ // Get all the recurring expiration emails to send in one month
+ $sent_emails['subscription_expiring_in_month'] = array();
+ $charges = $CI->recurring_model->GetChargesByExpiryDate($next_month);
+ if($charges) {
+ foreach($charges as $charge) {
+ if (empty($charge['renewed']) and empty($charge['updated'])) {
+ $this->app_hooks->data('subscription',$charge['subscription_id']);
+ $this->app_hooks->trigger('subscription_expire_1_month', $charge['subscription_id']);
+ $this->app_hooks->reset();
+
+ $sent_emails['subscription_expiring_in_month'][] = $charge['subscription_id'];
+ }
+ }
+ }
+
+ $charge_success = count($charge_success);
+ $charge_failure = count($charge_failure);
+ $cancelled = count($cancelled);
+ $expired = count($expired);
+ $autorecur_week = count($sent_emails['subscription_autorecur_in_week']);
+ $autorecur_month = count($sent_emails['subscription_autorecur_in_month']);
+ $expire_week = count($sent_emails['subscription_expiring_in_week']);
+ $expire_month = count($sent_emails['subscription_expiring_in_month']);
+
+ // report our cron accomplishments!
+ cron_log('Successful charges: ' . $charge_success);
+ cron_log('Failed Charges: ' . $charge_failure);
+ cron_log('Cancelled Subscriptions: ' . $cancelled);
+ cron_log('Expired Subscriptions: ' . $expired);
+ cron_log('Weekly Charge Reminders Sent: ' . $autorecur_week);
+ cron_log('Monthly Charge Reminders Sent: ' . $autorecur_month);
+ cron_log('Weekly Expiration Reminders Sent: ' . $expire_week);
+ cron_log('Monthly Expiration Reminders Sent: ' . $expire_month);
+
+ return TRUE;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/models/subscription_plan_model.php b/app/modules/billing/models/subscription_plan_model.php
new file mode 100644
index 00000000..80d6459c
--- /dev/null
+++ b/app/modules/billing/models/subscription_plan_model.php
@@ -0,0 +1,290 @@
+load->model('billing/plan_model');
+
+ if (empty($amount) and $initial_charge > 0) {
+ die(show_error('You can\'t create a plan that is free but only contains an initial charge. Use a product for that.'));
+ }
+
+ $params = array(
+ 'name' => $name,
+ 'plan_type' => (empty($amount)) ? 'free' : 'paid',
+ 'amount' => $amount,
+ 'interval' => $interval,
+ 'notification_url' => '',
+ 'occurrences' => $occurrences,
+ 'free_trial' => $free_trial
+ );
+
+ $plan_id = $CI->plan_model->NewPlan($params);
+
+ if (!$plan_id) {
+ die(show_error('Error creating plan record in `plans`.'));
+ }
+
+ $insert_fields = array(
+ 'plan_id' => $plan_id,
+ 'subscription_plan_initial_charge' => ($initial_charge == $amount or empty($initial_charge)) ? $amount : $initial_charge,
+ 'subscription_plan_is_taxable' => ($is_taxable == TRUE) ? '1' : '0',
+ 'subscription_plan_promotion' => $promotion,
+ 'subscription_plan_demotion' => $demotion,
+ 'subscription_plan_description' => $description,
+ 'subscription_plan_require_billing_trial' => ($require_billing_for_trial == FALSE) ? 0 : 1
+ );
+
+ $this->db->insert('subscription_plans',$insert_fields);
+
+ $subscription_plan_id = $this->db->insert_id();
+
+ return $subscription_plan_id;
+ }
+
+ /**
+ * Update Plan
+ *
+ * @param int $subscription_plan_id The subscription plan ID
+ * @param string $name Plan name
+ * @param float $amount Amount to charge on a recurring basis
+ * @param boolean|float $initial_charge Initial charge to bill, or FALSE if same as recurring charge. Cannot be 0 (use a free trial instead)
+ * @param boolean $is_taxable Apply tax rules?
+ * @param int $interval Number of days between charges, must be greater than 0
+ * @param int $free_trial Number of days to give a free trial
+ * @param boolean $require_billing_for_trial Require billing information for a trial?
+ * @param boolean|int $occurrences Number of occurrences, or FALSE/0 if unlimited.
+ * @param int $promotion Group to promote a user to upon subscription
+ * @param int $demotion Group to demota a user to upon expiration
+ * @param string $description Plan text description
+ *
+ * @return void
+ */
+ function update_plan ($subscription_plan_id, $name, $amount, $initial_charge, $is_taxable, $interval, $free_trial, $require_billing_for_trial, $occurrences, $promotion, $demotion, $description) {
+ $CI =& get_instance();
+
+ $CI->load->model('billing/plan_model');
+
+ if (empty($amount) and $initial_charge > 0) {
+ die(show_error('You can\'t create a plan that is free but only contains an initial charge. Use a product for that.'));
+ }
+
+ $params = array(
+ 'name' => $name,
+ 'plan_type' => (empty($amount)) ? 'free' : 'paid',
+ 'amount' => $amount,
+ 'interval' => $interval,
+ 'notification_url' => '',
+ 'occurrences' => $occurrences,
+ 'free_trial' => $free_trial
+ );
+
+ // get plan ID
+ $plan = $this->get_plan($subscription_plan_id);
+
+ if (!$CI->plan_model->UpdatePlan($plan['plan_id'], $params)) {
+ die(show_error('Unable to update plan record in `plans`.'));
+ }
+
+ $update_fields = array(
+ 'subscription_plan_initial_charge' => ($initial_charge == $amount or empty($initial_charge)) ? $amount : $initial_charge,
+ 'subscription_plan_is_taxable' => ($is_taxable == TRUE) ? '1' : '0',
+ 'subscription_plan_promotion' => $promotion,
+ 'subscription_plan_demotion' => $demotion,
+ 'subscription_plan_description' => $description,
+ 'subscription_plan_require_billing_trial' => ($require_billing_for_trial == FALSE) ? 0 : 1
+ );
+
+ $this->db->update('subscription_plans',$update_fields, array('subscription_plan_id' => $subscription_plan_id));
+
+ return TRUE;
+ }
+
+ /**
+ * Delete Plan
+ *
+ * @param int $id
+ *
+ * @return boolean
+ */
+ function delete_plan ($id) {
+ $update_data['deleted'] = 1;
+
+ $plan = $this->get_plan($id);
+
+ $this->db->where('plan_id', $plan['plan_id']);
+ $this->db->update('plans', $update_data);
+
+ return TRUE;
+ }
+
+ /**
+ * Get Plan
+ *
+ * @param int $id
+ *
+ * @return array
+ */
+ function get_plan ($id) {
+ if (isset($this->cache[$id])) {
+ return $this->cache[$id];
+ }
+
+ $result = $this->get_plans(array('id' => $id), TRUE);
+
+ if (empty($result)) {
+ return FALSE;
+ }
+
+ $this->cache[$id] = $result[0];
+ return $result[0];
+ }
+
+ /**
+ * Get Plan from API Plan ID
+ *
+ * @param int $api_plan_id
+ *
+ * @return array
+ */
+ function get_plan_from_api_plan_id ($api_plan_id) {
+ $result = $this->get_plans(array('api_plan_id' => $api_plan_id));
+
+ if (empty($result)) {
+ return FALSE;
+ }
+ else {
+ return $result[0];
+ }
+ }
+
+ /**
+ * Get Plans
+ *
+ * @param int $filters['id']
+ * @param float $filters['amount']
+ * @param int $filters['interval']
+ * @param string $filters['name']
+ * @param int $filters['api_plan_id']
+ * @param $allow_deleted (default: FALSE)
+ *
+ * @return array
+ */
+ function get_plans ($filters = array(), $allow_deleted = FALSE) {
+ if ($allow_deleted == FALSE) {
+ if (isset($filters['deleted']) and $filters['deleted'] == '1') {
+ $this->db->where('plans.deleted', '1');
+ }
+ else {
+ $this->db->where('plans.deleted', '0');
+ }
+ }
+
+ if (isset($filters['id'])) {
+ $this->db->where('subscription_plan_id', $filters['id']);
+ }
+
+ if (isset($filters['amount'])) {
+ $this->db->where('plans.amount', $filters['amount']);
+ }
+
+ if (isset($filters['interval'])) {
+ $this->db->where('interval', $filters['interval']);
+ }
+
+ if (isset($filters['name'])) {
+ $this->db->where('name', $filters['name']);
+ }
+
+ if (isset($filters['api_plan_id'])) {
+ $this->db->where('plans.plan_id',$filters['api_plan_id']);
+ }
+
+ // standard ordering and limiting
+ $order_by = (isset($filters['sort'])) ? $filters['sort'] : 'plans.name';
+ $order_dir = (isset($filters['sort_dir'])) ? $filters['sort_dir'] : 'ASC';
+ $this->db->order_by($order_by, $order_dir);
+
+ if (isset($filters['limit'])) {
+ $offset = (isset($filters['offset'])) ? $filters['offset'] : 0;
+ $this->db->limit($filters['limit'], $offset);
+ }
+
+ $this->db->select('subscription_plans.*');
+ $this->db->select('plans.*');
+ $this->db->select('SUM(subscriptions.active) as `active_subscribers`',false);
+ $this->db->join('plans','subscription_plans.plan_id = plans.plan_id','left');
+ $this->db->join('subscriptions','subscriptions.plan_id = plans.plan_id','left');
+ $this->db->group_by('subscription_plan_id');
+ $query = $this->db->get('subscription_plans');
+
+ $data = array();
+ if ($query->num_rows() > 0) {
+ foreach($query->result_array() as $row)
+ {
+ $data[] = array(
+ 'id' => $row['subscription_plan_id'], // for `subscription_plans`
+ 'plan_id' => $row['plan_id'], // for `plans`
+ 'name' => $row['name'],
+ 'type' => ($row['amount'] != '0.00') ? 'paid' : 'free',
+ 'initial_charge' => money_format("%!^i",$row['subscription_plan_initial_charge']),
+ 'amount' => money_format("%!^i",$row['amount']),
+ 'interval' => $row['interval'],
+ 'free_trial' => $row['free_trial'],
+ 'occurrences' => $row['occurrences'],
+ 'is_taxable' => ($row['subscription_plan_is_taxable'] == '1') ? TRUE : FALSE,
+ 'active_subscribers' => (empty($row['active_subscribers'])) ? '0' : $row['active_subscribers'],
+ 'deleted' => ($row['deleted'] == 1) ? TRUE : FALSE,
+ 'require_billing_for_trial' => ($row['subscription_plan_require_billing_trial'] == 1) ? TRUE : FALSE,
+ 'promotion' => $row['subscription_plan_promotion'],
+ 'demotion' => $row['subscription_plan_demotion'],
+ 'description' => $row['subscription_plan_description'],
+ 'add_to_cart' => site_url('billing/subscriptions/add_to_cart/' . $row['subscription_plan_id'])
+ );
+ }
+
+ } else {
+ return FALSE;
+ }
+
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/template_plugins/block.has_subscriptions.php b/app/modules/billing/template_plugins/block.has_subscriptions.php
new file mode 100644
index 00000000..b0bb03d2
--- /dev/null
+++ b/app/modules/billing/template_plugins/block.has_subscriptions.php
@@ -0,0 +1,18 @@
+CI->load->model('billing/subscription_model');
+ if ($smarty->CI->subscription_model->has_subscriptions()) {
+ return $tagdata;
+ }
+ else {
+ return '';
+ }
+}
\ No newline at end of file
diff --git a/app/modules/billing/template_plugins/block.subscription_plans.php b/app/modules/billing/template_plugins/block.subscription_plans.php
new file mode 100644
index 00000000..072d2860
--- /dev/null
+++ b/app/modules/billing/template_plugins/block.subscription_plans.php
@@ -0,0 +1,42 @@
+CI->smarty->loop_data_key($filters);
+
+ if ($smarty->CI->smarty->loop_data($data_name) === FALSE) {
+ // make content request
+ $smarty->CI->load->model('billing/subscription_plan_model');
+ $subs = $smarty->CI->subscription_plan_model->get_plans($filters);
+ }
+ else {
+ $subs = FALSE;
+ }
+
+ $smarty->CI->smarty->block_loop($data_name, $subs, (string)$params['var'], $repeat);
+ }
+
+ echo $tagdata;
+}
\ No newline at end of file
diff --git a/app/modules/billing/template_plugins/block.subscriptions.php b/app/modules/billing/template_plugins/block.subscriptions.php
new file mode 100644
index 00000000..37676fe9
--- /dev/null
+++ b/app/modules/billing/template_plugins/block.subscriptions.php
@@ -0,0 +1,60 @@
+CI->smarty->loop_data_key($filters);
+
+ if ($smarty->CI->smarty->loop_data($data_name) === FALSE) {
+ // make content request
+ $smarty->CI->load->model('billing/subscription_model');
+ $subs = $smarty->CI->subscription_model->get_subscriptions_friendly($filters);
+ }
+ else {
+ $subs = FALSE;
+ }
+
+ $smarty->CI->smarty->block_loop($data_name, $subs, (string)$params['var'], $repeat);
+ }
+
+ echo $tagdata;
+}
\ No newline at end of file
diff --git a/app/modules/billing/views/change_plan.php b/app/modules/billing/views/change_plan.php
new file mode 100644
index 00000000..0c7f349c
--- /dev/null
+++ b/app/modules/billing/views/change_plan.php
@@ -0,0 +1,11 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/gateway_details.php b/app/modules/billing/views/gateway_details.php
new file mode 100644
index 00000000..501bbcda
--- /dev/null
+++ b/app/modules/billing/views/gateway_details.php
@@ -0,0 +1,41 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+Is your gateway in test mode? Even when "Test mode" is specified below, your transactions can
+still be processed as live, real transactions if your gateway is not in test mode. You must set your gateway
+to test mode in your gateway control panel. "Test mode" below only indicates which of your gateway's servers to use.
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/gateways.php b/app/modules/billing/views/gateways.php
new file mode 100644
index 00000000..4db566a5
--- /dev/null
+++ b/app/modules/billing/views/gateways.php
@@ -0,0 +1,27 @@
+=$this->load->view(branded_view('cp/header'));?>
+Manage Gateways
+=$this->dataset->table_head();?>
+
+if (!empty($this->dataset->data)) {
+ foreach ($this->dataset->data as $row) {
+ ?>
+
+
+ =$row['id'];?>
+ =$row['gateway'];?>
+ =$row['date_created'];?>
+ edit
+ if (setting('default_gateway') == $row['id']) { ?>default } else { ?>
+ make default } ?>
+
+
+ }
+}
+else {
+?>
+Empty data set.
+
+}
+?>
+=$this->dataset->table_close();?>
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/new_gateway_type.php b/app/modules/billing/views/new_gateway_type.php
new file mode 100644
index 00000000..6e8dcc6a
--- /dev/null
+++ b/app/modules/billing/views/new_gateway_type.php
@@ -0,0 +1,23 @@
+=$this->load->view(branded_view('cp/header')); ?>
+Setup New Gateway
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/new_subscription.php b/app/modules/billing/views/new_subscription.php
new file mode 100644
index 00000000..694d37df
--- /dev/null
+++ b/app/modules/billing/views/new_subscription.php
@@ -0,0 +1,11 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/new_subscription_2.php b/app/modules/billing/views/new_subscription_2.php
new file mode 100644
index 00000000..eb591400
--- /dev/null
+++ b/app/modules/billing/views/new_subscription_2.php
@@ -0,0 +1,163 @@
+=$this->head_assets->javascript('js/form.address.js');?>
+=$this->head_assets->javascript('js/form.transaction.js');?>
+
+=$this->load->view(branded_view('cp/header')); ?>
+=$form_title;?>
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/subscription_form.php b/app/modules/billing/views/subscription_form.php
new file mode 100644
index 00000000..3e1c0b80
--- /dev/null
+++ b/app/modules/billing/views/subscription_form.php
@@ -0,0 +1,95 @@
+
+
+/* Default Values */
+
+if (!isset($form)) {
+ $form = array(
+ 'name' => '',
+ 'type' => 'free',
+ 'amount' => '0',
+ 'interval' => '30',
+ 'notification_url' => 'http://',
+ 'occurrences' => '0',
+ 'free_trial' => '0',
+ 'initial_charge' => '',
+ 'require_billing_for_trial' => '1',
+ 'is_taxable' => '1'
+ );
+
+}
+
+?>
+
+=$this->head_assets->javascript('js/form.plan.js');?>
+
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/subscriptions.php b/app/modules/billing/views/subscriptions.php
new file mode 100644
index 00000000..7d5d13e1
--- /dev/null
+++ b/app/modules/billing/views/subscriptions.php
@@ -0,0 +1,30 @@
+=$this->load->view(branded_view('cp/header'));?>
+Subscription Plans
+=$this->dataset->table_head();?>
+
+if (!empty($this->dataset->data)) {
+ foreach ($this->dataset->data as $row) {
+ ?>
+
+
+ =$row['id'];?>
+ =$row['name'];?>
+ =setting('currency_symbol');?>=$row['amount'];?>
+ =$row['interval'];?> days
+ if (!empty($row['free_trial'])) { ?>=$row['free_trial'];?> days } else { ?>none } ?>
+ =$row['active_subscribers'];?>
+ if (!empty($row['promotion'])) { ?>=$usergroups[$row['promotion']];?> } ?>
+ if (!empty($row['demotion'])) { ?>=$usergroups[$row['demotion']];?> } ?>
+ edit
+
+
+ }
+}
+else {
+?>
+
+ No subscription plans in this dataset.
+
+ } ?>
+=$this->dataset->table_close();?>
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/billing/views/update_cc.php b/app/modules/billing/views/update_cc.php
new file mode 100644
index 00000000..15878f01
--- /dev/null
+++ b/app/modules/billing/views/update_cc.php
@@ -0,0 +1,119 @@
+=$this->head_assets->javascript('js/form.address.js');?>
+=$this->head_assets->javascript('js/form.transaction.js');?>
+
+=$this->load->view(branded_view('cp/header')); ?>
+=$form_title;?>
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/coupons/controllers/admincp.php b/app/modules/coupons/controllers/admincp.php
new file mode 100644
index 00000000..e0b66dce
--- /dev/null
+++ b/app/modules/coupons/controllers/admincp.php
@@ -0,0 +1,385 @@
+admin_navigation->parent_active('storefront');
+
+ $this->load->model('coupon_model');
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Manage Coupons
+ *
+ * Displays the list of current coupons.
+ */
+ public function index()
+ {
+ $this->admin_navigation->module_link('Add Coupon',site_url('admincp/coupons/add'));
+
+ $this->load->library('dataset');
+
+ // Get coupon types
+ $coupon_types = $this->coupon_model->get_coupon_types();
+ foreach ($coupon_types as $type)
+ {
+ $coupon_options[$type->coupon_type_id] = $type->coupon_type_name;
+ }
+
+ $columns = array(
+ array(
+ 'name' => 'ID #',
+ 'type' => 'id',
+ 'width' => '5%',
+ 'filter' => 'id'),
+ array(
+ 'name' => 'Coupon Name',
+ 'sort_column' => 'coupon_name',
+ 'type' => 'text',
+ 'width' => '20%',
+ 'filter' => 'name'),
+ array(
+ 'name' => 'Code',
+ 'type' => 'text',
+ 'width' => '15%',
+ 'filter' => 'code'
+ ),
+ array(
+ 'name' => 'Active Dates',
+ 'type' => 'date',
+ 'sort_column' => 'coupon_start_date',
+ 'width' => '15%',
+ 'filter' => 'timestamp',
+ 'field_start_date' => 'start_date',
+ 'field_end_date' => 'end_date'
+ ),
+ array(
+ 'name' => 'Coupon Type',
+ 'type' => 'select',
+ 'options' => $coupon_options,
+ 'width' => '15%',
+ 'filter' => 'type'
+ )
+ );
+
+ $this->dataset->columns($columns);
+ $this->dataset->datasource('coupon_model','get_coupons');
+ $this->dataset->base_url(site_url('admincp/coupons'));
+ $this->dataset->rows_per_page(1000);
+
+ // total rows
+ $this->db->select('coupon_id');
+ $this->db->where('coupon_deleted', '0');
+ $total_rows = $this->db->get('coupons')->num_rows();
+ $this->dataset->total_rows($total_rows);
+
+ // initialize the dataset
+ $this->dataset->initialize();
+
+ // add actions
+ $this->dataset->action('Delete','admincp/coupons/delete_coupons');
+
+ $this->load->view('coupons', array('coupon_options'=>$coupon_options));
+ }
+
+ //--------------------------------------------------------------------
+
+ public function add()
+ {
+ $form = $this->build_coupon_form();
+
+ // Prep our page
+ $data = array(
+ 'form' => $form->display(),
+ 'form_title' => 'Create New Coupon',
+ 'action' => 'new',
+ 'form_action' => site_url('admincp/coupons/post_coupon/new')
+ );
+
+ $this->load->view('add', $data);
+ }
+
+ //--------------------------------------------------------------------
+
+ public function edit($id=0)
+ {
+ // Grab our coupon data
+ $coupon = $this->coupon_model->get_coupon($id);
+
+ $form = $this->build_coupon_form($coupon, $id);
+
+ // Prep our page
+ $data = array(
+ 'form' => $form->display(),
+ 'form_title' => 'Edit Coupon',
+ 'action' => 'edit',
+ 'form_action' => site_url('admincp/coupons/post_coupon/edit/'.$id)
+ );
+
+ $this->load->view('edit', $data);
+ }
+
+ //--------------------------------------------------------------------
+
+
+ public function post_coupon($action = 'edit', $id = FALSE)
+ {
+ $editing = $action == 'edit' ? TRUE : FALSE;
+
+ $validated = $this->coupon_model->validation($editing);
+ if ($validated !== TRUE) {
+ $this->notices->SetError(implode('
',$validated));
+ $error = TRUE;
+ }
+
+ if (isset($error)) {
+ if ($action == 'new') {
+ redirect('admincp/coupons/add');
+ return FALSE;
+ }
+ else {
+ redirect('admincp/coupons/edit/' . $id);
+ return FALSE;
+ }
+ }
+
+ if ($action == 'new')
+ {
+ // New coupon
+ $coupon = $this->build_coupon_form_input();
+ $coupon_id = $this->coupon_model->new_coupon(
+ $coupon['coupon_name'],
+ $coupon['coupon_code'],
+ $coupon['coupon_start_date'],
+ $coupon['coupon_end_date'],
+ $coupon['coupon_max_uses'],
+ $coupon['coupon_customer_limit'],
+ $coupon['coupon_type_id'],
+ $coupon['coupon_reduction_type'],
+ $coupon['coupon_reduction_amt'],
+ $coupon['coupon_trial_length'],
+ $coupon['coupon_min_cart_amt'],
+ $coupon['products'],
+ $coupon['plans'],
+ $coupon['ship_rates']
+ );
+
+ if ($coupon_id)
+ {
+ $this->notices->SetNotice('Coupon added successfully.');
+ } else
+ {
+ $this->notices->SetError('Unable to create coupon.');
+ }
+ } else
+ {
+ // Edited coupon
+ $coupon_id = $this->input->post('coupon_id');
+ $coupon = $this->build_coupon_form_input();
+ $result = $this->coupon_model->update_coupon(
+ $coupon_id,
+ $coupon['coupon_name'],
+ $coupon['coupon_code'],
+ $coupon['coupon_start_date'],
+ $coupon['coupon_end_date'],
+ $coupon['coupon_max_uses'],
+ $coupon['coupon_customer_limit'],
+ $coupon['coupon_type_id'],
+ $coupon['coupon_reduction_type'],
+ $coupon['coupon_reduction_amt'],
+ $coupon['coupon_trial_length'],
+ $coupon['coupon_min_cart_amt'],
+ $coupon['products'],
+ $coupon['plans'],
+ $coupon['ship_rates']
+ );
+
+ if ($result)
+ {
+ $this->notices->SetNotice('Coupon saved successfully.');
+ } else
+ {
+ $this->notices->SetError('Unable to save coupon.');
+ }
+ }
+
+ redirect('admincp/coupons');
+
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+ function delete_coupons ($coupons, $return_url) {
+ $this->load->library('asciihex');
+
+ $coupons = unserialize(base64_decode($this->asciihex->HexToAscii($coupons)));
+ $return_url = base64_decode($this->asciihex->HexToAscii($return_url));
+
+ foreach ($coupons as $coupon) {
+ $this->coupon_model->delete_coupon($coupon);
+ }
+
+ $this->notices->SetNotice('Coupon(s) deleted successfully.');
+
+ redirect($return_url);
+
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+ // PRIVATE METHODS
+ //--------------------------------------------------------------------
+
+ private function build_coupon_form($coupon=array(), $id=0)
+ {
+ $this->load->model('store/shipping_model');
+ $this->load->model('store/products_model');
+ $this->load->model('billing/subscription_plan_model');
+
+ // Get the required options
+ $coupon_products = isset($coupon['products']) ? $coupon['products'] : array();
+ $coupon_plans = isset($coupon['plans']) ? $coupon['plans'] : array();
+ $coupon_shipping = isset($coupon['shipping']) ? $coupon['shipping'] : array();
+
+ $coupon_types = $this->coupon_model->get_coupon_types();
+
+ foreach ($coupon_types as $type)
+ {
+ $type_options[$type->coupon_type_id] = $type->coupon_type_name;
+ }
+
+ $reduction_options = array(
+ 0 => '%',
+ 1 => setting('currency_symbol')
+ );
+
+ $products = $this->products_model->get_products();
+ $product_options = array();
+ $product_options['-1'] = 'not available for any products';
+
+ if (is_array($products)) {
+ foreach ($products as $product)
+ {
+ $product_options[$product['id']] = $product['name'];
+ }
+ }
+
+ $plans = $this->subscription_plan_model->get_plans();
+ $plan_options = array();
+ $plan_options['-1'] = 'not available for any subscriptions';
+
+ if (is_array($plans)) {
+ foreach ($plans as $plan)
+ {
+ $plan_options[$plan['id']] = $plan['name'];
+ }
+ }
+
+ $shipping = $this->shipping_model->get_rates();
+ $shipping_options = array();
+
+ if (is_array($shipping)) {
+ foreach ($shipping as $rate)
+ {
+ $shipping_options[$rate['id']] = $rate['name'];
+ }
+ }
+
+ // Build the form
+ $this->load->library('admin_form');
+
+ $form = new Admin_form;
+ $form->fieldset('Coupon Information');
+ $form->hidden('coupon_id', $id);
+ $form->text('Coupon Name', 'coupon_name', isset($coupon['coupon_name']) ? $coupon['coupon_name'] : null, 'Something for you to recognize the coupon by.', TRUE);
+ $form->text('Coupon Code', 'coupon_code', isset($coupon['coupon_code']) ? $coupon['coupon_code'] : null, 'The code the customer must enter.', TRUE);
+ $form->date('Start Date', 'coupon_start_date', isset($coupon['coupon_start_date']) ? $coupon['coupon_start_date'] : null, null, TRUE, FALSE, FALSE, '8em');
+ $form->date('Expiry Date', 'coupon_end_date', isset($coupon['coupon_end_date']) ? $coupon['coupon_end_date'] : null, null, TRUE, FALSE, FALSE, '8em');
+ $form->text('Maximum Uses', 'coupon_max_uses', (isset($coupon['coupon_max_uses']) and !empty($coupon['coupon_max_uses'])) ? $coupon['coupon_max_uses'] : null, 'The maximum number of customers that can use the coupon.', FALSE, FALSE, FALSE, '6em');
+ $form->checkbox('One Per Customer?', 'coupon_customer_limit', '1', isset($coupon['coupon_customer_limit']) && $coupon['coupon_customer_limit'] == 1 ? TRUE : FALSE, 'Check to limit each customer to a single use.');
+ $form->dropdown('Coupon Type', 'coupon_type_id', $type_options, isset($coupon['coupon_type_id']) ? $coupon['coupon_type_id'] : FALSE, FALSE, FALSE, FALSE, FALSE, 'coupon_type');
+
+ $form->fieldset('Price Reduction', array('coupon_reduction'));
+ $form->dropdown('Reduction Type', 'coupon_reduction_type', $reduction_options, isset($coupon['coupon_reduction_type']) ? $coupon['coupon_reduction_type'] : FALSE);
+ $form->text('Reduction Amount', 'coupon_reduction_amt', isset($coupon['coupon_reduction_amt']) ? $coupon['coupon_reduction_amt'] : null, 'The amount of the discount.', FALSE, FALSE, FALSE, '6em');
+ $form->dropdown('Products', 'products[]', $product_options, $coupon_products, TRUE, FALSE, 'Leave all unselected to make available for all products.');
+ $form->dropdown('Subscription Plans', 'plans[]', $plan_options, $coupon_plans, TRUE, FALSE, 'Leave all unselected to make available for all subscriptions.');
+
+ $form->fieldset('Free Trial', array('coupon_trial'));
+ $form->text('Free Trial Length', 'coupon_trial_length', isset($coupon['coupon_trial_length']) ? $coupon['coupon_trial_length'] : null, null, FALSE, 'in days', FALSE, '6em');
+ $form->dropdown('Subscription Plans', 'trial_subs[]', $plan_options, $coupon_plans, TRUE, 'If left blank, will select ALL SUBSCRIPTION PLANS');
+
+ $form->fieldset('Free Shipping', array('coupon_shipping'));
+ $form->text('Min. Cart Amount', 'coupon_min_cart_amt', isset($coupon['coupon_min_cart_amt']) ? $coupon['coupon_min_cart_amt'] : null, 'The minimum order amount before the coupon may be used.', FALSE, FALSE, FALSE, '6em');
+ $form->dropdown('Shipping Methods', 'ship_rates[]', $shipping_options, $coupon_shipping, TRUE, 'If left blank, will select ALL SHIPPING PLANS');
+
+ return $form;
+ }
+
+ //--------------------------------------------------------------------
+
+
+ /**
+ * builds the coupon item from form input data.
+ * This is used by both add and edit forms.
+ */
+ private function build_coupon_form_input()
+ {
+ $coupon = array(
+ 'coupon_name' => $this->input->post('coupon_name'),
+ 'coupon_code' => $this->input->post('coupon_code'),
+ 'coupon_start_date' => $this->input->post('coupon_start_date'),
+ 'coupon_end_date' => $this->input->post('coupon_end_date'),
+ 'coupon_max_uses' => $this->input->post('coupon_max_uses'),
+ 'coupon_customer_limit' => $this->input->post('coupon_customer_limit'),
+ 'coupon_type_id' => $this->input->post('coupon_type_id'),
+ 'coupon_reduction_type' => null, // Null the following so we at least have placeholder values.
+ 'coupon_reduction_amt' => null,
+ 'coupon_trial_length' => null,
+ 'coupon_min_cart_amt' => null,
+ 'products' => null,
+ 'plans' => null,
+ 'ship_rates' => null
+ );
+
+
+ switch($this->input->post('coupon_type_id')) {
+ // Price Reduction
+ case 1:
+ $coupon['coupon_reduction_type'] = $this->input->post('coupon_reduction_type');
+ $coupon['coupon_reduction_amt'] = $this->input->post('coupon_reduction_amt');
+ $coupon['products'] = $this->input->post('products');
+ $coupon['plans'] = $this->input->post('plans');
+ break;
+
+ // Free Trial
+ case 2:
+ $coupon['coupon_trial_length'] = $this->input->post('coupon_trial_length');
+ $coupon['plans'] = $this->input->post('trial_subs');
+ break;
+
+ // Free Subscription
+ case 3:
+ $coupon['coupon_min_cart_amt'] = $this->input->post('coupon_min_cart_amt');
+ $coupon['ship_rates'] = $this->input->post('ship_rates');
+ break;
+ }
+
+ return $coupon;
+ }
+
+ //--------------------------------------------------------------------
+
+}
+
+/* End of file admincp.php */
+/* Location: ./app/modules/coupons/admincp.php */
\ No newline at end of file
diff --git a/app/modules/coupons/coupons.php b/app/modules/coupons/coupons.php
new file mode 100644
index 00000000..e2384f3e
--- /dev/null
+++ b/app/modules/coupons/coupons.php
@@ -0,0 +1,155 @@
+active_module = $this->name;
+ parent::__construct();
+ }
+
+ /**
+ * Pre-admin function
+ *
+ * Initiate navigation in control panel
+ */
+ public function admin_preload()
+ {
+ $this->CI->admin_navigation->child_link('storefront', 31, 'Coupons', site_url('admincp/coupons/'));
+ }
+
+ /**
+ * Module update
+ *
+ * @param int $db_version The current DB version
+ * @return int The current software version, to update the database
+ */
+ public function update($db_version)
+ {
+ if ($db_version < 1.0) {
+ // Initial install
+
+ // Base Coupons
+ $this->CI->db->query("CREATE TABLE `coupons` (
+ `coupon_id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
+ `coupon_type_id` INT( 11 ) UNSIGNED NOT NULL ,
+ `coupon_name` VARCHAR(60) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+ `coupon_code` VARCHAR( 20 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
+ `coupon_start_date` DATE NOT NULL ,
+ `coupon_end_date` DATE NOT NULL ,
+ `coupon_max_uses` INT( 11 ) UNSIGNED NOT NULL ,
+ `coupon_customer_limit` INT( 11 ) UNSIGNED NOT NULL ,
+ `coupon_reduction_type` TINYINT( 1 ) UNSIGNED NULL COMMENT '0=%, 1=fixed amount',
+ `coupon_reduction_amt` INT( 9 ) UNSIGNED NULL ,
+ `coupon_trial_length` INT( 4 ) UNSIGNED NULL COMMENT 'in days',
+ `coupon_min_cart_amt` INT( 9 ) UNSIGNED NULL COMMENT 'in cents',
+ `coupon_deleted` TINYINT( 1 ) UNSIGNED NOT NULL,
+ `created_on` DATETIME NOT NULL ,
+ `modified_on` DATETIME NULL
+ ) ENGINE = MYISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;");
+
+ // Coupon Types
+ $this->CI->db->query('CREATE TABLE `coupon_types` (
+ `coupon_type_id` INT( 3 ) NOT NULL ,
+ `coupon_type_name` VARCHAR( 255 ) NOT NULL ,
+ INDEX ( `coupon_type_id` )
+ ) ENGINE = MYISAM CHARACTER SET utf8 COLLATE utf8_general_ci;');
+
+ $this->CI->db->query("INSERT INTO `coupon_types` (`coupon_type_id`, `coupon_type_name`) VALUES ('1', 'Price Reduction'), ('2', 'Free Trial'), ('3', 'Free Shipping');");
+
+ // Coupon/Product Relationships
+ $this->CI->db->query("CREATE TABLE `coupons_products` (
+ `coupon_id` INT( 11 ) UNSIGNED NOT NULL ,
+ `product_id` INT( 11 ) UNSIGNED NOT NULL ,
+ INDEX ( `coupon_id` , `product_id` )
+ ) ENGINE = MYISAM ;");
+
+ // Coupon/Subscription Relationships
+ $this->CI->db->query("CREATE TABLE `coupons_subscriptions` (
+ `coupon_id` INT( 11 ) NOT NULL ,
+ `subscription_plan_id` INT( 11 ) NOT NULL ,
+ INDEX ( `coupon_id` , `subscription_plan_id` )
+ ) ENGINE = MYISAM ;");
+
+ // Coupon/Shipping Method Relationships
+ $this->CI->db->query("CREATE TABLE `coupons_shipping` (
+ `coupon_id` INT( 11 ) UNSIGNED NOT NULL ,
+ `shipping_id` INT( 11 ) NOT NULL ,
+ INDEX ( `coupon_id` , `shipping_id` )
+ ) ENGINE = MYISAM ;");
+
+ }
+
+ if ($db_version < 1.01) {
+ $this->CI->db->query('UPDATE `coupon_types` SET `coupon_type_name`=\'Free Shipping\' WHERE `coupon_type_name`=\'Free Subscription\'');
+ }
+
+ if ($db_version < 1.04) {
+ $this->CI->db->query('ALTER TABLE `coupons` MODIFY COLUMN `coupon_reduction_amt` FLOAT UNSIGNED');
+ }
+
+ if ($db_version < 1.05) {
+ $this->CI->db->query('ALTER TABLE `coupons_products` MODIFY COLUMN `product_id` FLOAT');
+ $this->CI->db->query('ALTER TABLE `coupons_subscriptions` MODIFY COLUMN `subscription_plan_id` FLOAT');
+ }
+
+ if ($db_version < 1.06) {
+ $this->CI->load->library('app_hooks');
+ $this->CI->app_hooks->register('coupon_validate','A user is attempting to apply a coupon during checkout.',array('member'));
+ }
+
+ // return current version
+ return $this->version;
+ }
+
+
+ /**
+ * Uninstall
+ *
+ * @return boolean
+ */
+ function uninstall () {
+ $delete_tables = array(
+ 'coupons',
+ 'coupon_types',
+ 'coupons_products',
+ 'coupons_subscriptions',
+ 'coupons_shipping'
+ );
+
+ $delete_hooks = array(
+ 'coupon_validate'
+ );
+
+ if (!empty($delete_tables)) {
+ foreach ($delete_tables as $table) {
+ $this->CI->db->query('DROP TABLE IF EXISTS `' . $table . '`');
+ }
+ }
+
+ if (!empty($delete_hooks)) {
+ foreach ($delete_hooks as $hook) {
+ $this->CI->db->query('DELETE FROM `hooks` WHERE `hook_name`=\'' . $hook . '\'');
+ }
+ }
+
+ return TRUE;
+ }
+
+}
+
+/* End of file coupons.php */
+/* Location: ./app/modules/coupons/coupons.php */
\ No newline at end of file
diff --git a/app/modules/coupons/models/coupon_model.php b/app/modules/coupons/models/coupon_model.php
new file mode 100644
index 00000000..4721f31c
--- /dev/null
+++ b/app/modules/coupons/models/coupon_model.php
@@ -0,0 +1,598 @@
+db->select('coupon_id')
+ ->where('coupon_deleted','0')
+ ->where('coupon_end_date >=',date('Y-m-d'));
+ $result = $this->db->get('coupons');
+
+ if ($result->num_rows() > 0) {
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Count Uses
+ *
+ * @param $coupon_id
+ *
+ * @return int $number_of_uses
+ */
+ function count_uses ($coupon_id) {
+ $result = $this->db->select('subscription_id')
+ ->from('subscriptions')
+ ->where('coupon_id',$coupon_id)
+ ->get();
+
+
+ $subscriptions = $result->num_rows();
+
+ $result = $this->db->select('order_details_id')
+ ->from('order_details')
+ ->where('coupon_id',$coupon_id)
+ ->get();
+
+
+ $orders = $result->num_rows();
+
+ return (int)$subscriptions + $orders;
+ }
+
+ /**
+ * Customer Usage
+ *
+ * @param int $coupon_id
+ * @param int $customer_id
+ *
+ * @return int $number_of_uses
+ */
+ function customer_usage ($coupon_id, $customer_id) {
+ $result = $this->db->select('subscription_id')
+ ->from('subscriptions')
+ ->where('coupon_id',$coupon_id)
+ ->where('customer_id',$customer_id)
+ ->get();
+
+
+ $subscriptions = $result->num_rows();
+
+ $result = $this->db->select('order_details_id')
+ ->from('order_details')
+ ->where('coupon_id',$coupon_id)
+ ->where('customer_id',$customer_id)
+ ->get();
+
+
+ $orders = $result->num_rows();
+
+ return (int)$subscriptions + $orders;
+ }
+
+ /**
+ * Get a single coupon.
+ *
+ * @param int $id The coupon_id to find.
+ *
+ * @return array The coupon details, or FALSE on coupon not found.
+ */
+ public function get_coupon ($id)
+ {
+ $this->db->where('coupon_deleted', 0);
+ $this->db->where('coupon_id', $id);
+ $query = $this->db->get('coupons');
+
+ if ($query->num_rows())
+ {
+ $coupon = $query->row_array();
+
+ // Get any associated products
+ $coupon['products'] = $this->get_related($id, 'coupons_products', 'product_id');
+
+ // Get associated subscription plans
+ $coupon['plans'] = $this->get_related($id, 'coupons_subscriptions', 'subscription_plan_id');
+
+ // Get associated shipping
+ $coupon['shipping'] = $this->get_related($id, 'coupons_shipping', 'shipping_id');
+
+ return $coupon;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Count Coupons
+ */
+ function count_coupons ($filters = array()) {
+ return $this->get_coupons($filters, TRUE);
+ }
+
+ /**
+ * Get Coupon Usages
+ *
+ * @param $filters['code']
+ *
+ * @return array
+ */
+ function get_coupon_usages ($filters = array()) {
+ if (isset($filters['code'])) {
+ $this->db->where('coupon_code', $filters['code']);
+ }
+
+ if (isset($filters['code_search'])) {
+ $this->db->like('coupon_code', $filters['code_search']);
+ }
+
+ $this->db->select('coupons.*');
+
+ $this->db->select('COUNT(`order_details`.`coupon_id`) AS `order_usages`');
+ $this->db->select('COUNT(`subscriptions`.`coupon_id`) AS `subscription_usages`');
+
+ $this->db->join('order_details','order_details.coupon_id = coupons.coupon_id','left');
+ $this->db->join('subscriptions','coupons.coupon_id = subscriptions.coupon_id','left');
+
+ // standard ordering and limiting
+ $order_by = (isset($filters['sort'])) ? $filters['sort'] : 'order_usages';
+ $order_dir = (isset($filters['sort_dir'])) ? $filters['sort_dir'] : 'DESC';
+
+ $this->db->order_by($order_by, $order_dir);
+
+ if (isset($filters['limit'])) {
+ $offset = (isset($filters['offset'])) ? $filters['offset'] : 0;
+ $this->db->limit($filters['limit'], $offset);
+ }
+
+ $this->db->group_by('coupons.coupon_id');
+
+ $this->db->from('coupons');
+
+ $result = $this->db->get();
+
+ if ($result->num_rows() == 0) {
+ return FALSE;
+ }
+
+ $coupons = array();
+
+ foreach ($result->result_array() as $coupon) {
+ $coupons[] = array(
+ 'id' => $coupon['coupon_id'],
+ 'order_usages' => $coupon['order_usages'],
+ 'subscription_usages' => $coupon['subscription_usages'],
+ 'total_usages' => (int)($coupon['order_usages'] + $coupon['subscription_usages']),
+ 'type_id' => $coupon['coupon_type_id'],
+ 'name' => $coupon['coupon_name'],
+ 'code' => $coupon['coupon_code'],
+ 'start_date' => $coupon['coupon_start_date'],
+ 'end_date' => $coupon['coupon_end_date'],
+ 'max_uses' => $coupon['coupon_max_uses'],
+ 'customer_limit' => $coupon['coupon_customer_limit'],
+ 'reduction_type' => $coupon['coupon_reduction_type'],
+ 'reduction_amt' => $coupon['coupon_reduction_amt'],
+ 'trial_length' => $coupon['coupon_trial_length'],
+ 'min_cart_amt' => $coupon['coupon_min_cart_amt']
+ );
+ }
+
+ return $coupons;
+ }
+
+
+ /**
+ * Get a list of coupons
+ *
+ * @param int $filters['id'] Coupon ID
+ * @param string $filters['name'] The name of the coupon
+ * @param string $filters['code'] The coupon code (entered at checkout)
+ * @param string $filters['code_search'] (like above, with LIKE)
+ * @param date $filters['start_date'] Start date of coupon must be after or equal to this date
+ * @param date $filters['end_date'] Start date of coupon must be before or equal to this date
+ * @param int $filters['type'] Type of coupon (1 = Price Reduction, 2 = Free Trial, 3 = Free Shipping)
+ *
+ * @return array The coupons that match the filters.
+ */
+ public function get_coupons ($filters=array(), $counting = FALSE)
+ {
+
+ //--------------------------------------------------------------------
+ // setup filters
+ //--------------------------------------------------------------------
+
+ // ID
+ if (isset($filters['id']))
+ {
+ $this->db->where('coupon_id',$filters['id']);
+ }
+
+ // Name
+ if (isset($filters['name']))
+ {
+ $this->db->like('coupon_name', $filters['name']);
+ }
+
+ // Code
+ if (isset($filters['code']) && !empty($filters['code']))
+ {
+ // not a LIKE, this must be exact!
+ $this->db->where('coupon_code', $filters['code']);
+ }
+
+ if (isset($filters['code_search']) && !empty($filters['code_search']))
+ {
+ $this->db->like('coupon_code', $filters['code_search']);
+ }
+
+ // Start Date
+ if (isset($filters['start_date']))
+ {
+ $this->db->where('coupon_start_date >=', $filters['start_date'] );
+ }
+ // End Date
+ if (isset($filters['end_date']))
+ {
+ $this->db->where('coupon_start_date <=', $filters['end_date'] );
+ }
+
+ // Reduction Type
+ if (isset($filters['type']))
+ {
+ $this->db->where('coupon_type_id', $filters['type'] );
+ }
+
+ $this->db->where('coupon_deleted', 0);
+
+ if ($counting == FALSE) {
+ // standard ordering and limiting
+ $order_by = (isset($filters['sort'])) ? $filters['sort'] : 'coupons.coupon_id';
+ $order_dir = (isset($filters['sort_dir'])) ? $filters['sort_dir'] : 'DESC';
+
+ $this->db->order_by($order_by, $order_dir);
+
+ if (isset($filters['limit'])) {
+ $offset = (isset($filters['offset'])) ? $filters['offset'] : 0;
+ $this->db->limit($filters['limit'], $offset);
+ }
+
+ $this->db->from('coupons');
+
+ $result = $this->db->get();
+ }
+ else {
+ $this->db->select('COUNT(coupons.coupon_id) AS `counted`');
+
+ $result = $this->db->get('coupons');
+ $count = $result->row_array();
+ return $count['counted'];
+ }
+
+ if ($result->num_rows() === 0)
+ {
+ return FALSE;
+ }
+
+ $coupons = array();
+ foreach ($result->result_array() as $row)
+ {
+ $coupons[] = array(
+ 'id' => $row['coupon_id'],
+ 'type_id' => $row['coupon_type_id'],
+ 'name' => $row['coupon_name'],
+ 'code' => $row['coupon_code'],
+ 'start_date' => $row['coupon_start_date'],
+ 'end_date' => $row['coupon_end_date'],
+ 'max_uses' => $row['coupon_max_uses'],
+ 'customer_limit' => $row['coupon_customer_limit'],
+ 'reduction_type' => $row['coupon_reduction_type'],
+ 'reduction_amt' => $row['coupon_reduction_amt'],
+ 'trial_length' => $row['coupon_trial_length'],
+ 'min_cart_amt' => $row['coupon_min_cart_amt']
+ );
+ }
+
+ return $coupons;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Returns an object containing all of the coupon types in the system.
+ *
+ * @return object The coupon types in system, or FALSE if none exist.
+ */
+ public function get_coupon_types ()
+ {
+ $query = $this->db->get('coupon_types');
+
+ if ($query->num_rows() > 0)
+ {
+ return $query->result();
+ }
+
+ return FALSE;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Validates POST data to be appropriate for a coupon.
+ *
+ * @param boolean $editing Whether in editing/new mode.
+ *
+ * @return boolean Whether it was successful or not.
+ */
+ public function validation ($editing=TRUE)
+ {
+ $this->load->library('form_validation');
+
+ $this->form_validation->set_rules('coupon_name', 'Coupon Name', 'trim|required|max_length[60]');
+ $this->form_validation->set_rules('coupon_code', 'Coupon Code', 'trim|required|mx_length[20]');
+ $this->form_validation->set_rules('coupon_start_date', 'Start Date', 'trim|required');
+ $this->form_validation->set_rules('coupon_end_date', 'End Date', 'trim|required');
+ $this->form_validation->set_rules('coupon_max_uses', 'Maximum Uses', 'trim|is_natural');
+ $this->form_validation->set_rules('coupon_type_id', 'Coupon Type', 'trim|is_natural');
+
+ switch ($this->input->post('coupon_type_id'))
+ {
+ // Price Reduction
+ case 1:
+ $this->form_validation->set_rules('coupon_reduction_amt', 'Reduction Amount', 'trim|required|numeric');
+ break;
+ // Free Trial
+ case 2:
+ $this->form_validation->set_rules('coupon_trial_length', 'Free Trial Length', 'trim|required|is_natural');
+ $this->form_validation->set_rules('trial_subs[]', 'Subscription Plans', 'required');
+ break;
+ // Free Shipping
+ case 3:
+ $this->form_validation->set_rules('coupon_min_cart_amt', 'Min. Cart Amount', 'trim|required|numeric');
+ $this->form_validation->set_rules('ship_rates[]', 'Shipping Methods', 'required');
+ break;
+ }
+
+
+ if ($this->form_validation->run() == FALSE) {
+ $errors = rtrim(validation_errors('','||'),'|');
+ $errors = explode('||',str_replace('','',$errors));
+ return $errors;
+ }
+
+ return TRUE;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Creates a new coupon in the database.
+ *
+ * @param string $name The coupon name
+ * @param string $code The coupon code
+ * @param string $start_date The first day that the coupon can be used (ie YYYY-MM-DD)
+ * @param string $end_date The last day the coupon can be used
+ * @param int $max_uses The maximum number of times the coupon can be used
+ * @param bool $customer_limit Whether or not to restrict the customer to a single use
+ * @param int $type_id The type of coupon (Price reduction, free trial or free subscription)
+ * @param int $reduction_type Whether percent or fixed amount of reduction. Only applicable for Price Reductions.
+ * @param string $reduction_amt How much to reduce price by. Only applicable for Price Reduction.
+ * @param int $trial_length How many days the free trial will last.
+ * @param string $min_amt The minimum amount in the cart before they qualify for free shipping.
+ * @param array $products An array of product ids to assign this coupon to.
+ * @param array $plans An array of subscription plans to assign this coupon to.
+ * @param array $ship_rates An array of shipping rates to apply this coupon to.
+ *
+ * @return int The id of the new coupon, or FALSE on failure.
+ */
+ public function new_coupon ($name, $code, $start_date, $end_date, $max_uses, $customer_limit, $type_id, $reduction_type, $reduction_amt, $trial_length, $min_amt, $products, $plans, $ship_rates)
+ {
+ $insert_fields = array(
+ 'coupon_name' => $name,
+ 'coupon_code' => $code,
+ 'coupon_start_date' => $start_date,
+ 'coupon_end_date' => $end_date,
+ 'coupon_max_uses' => $max_uses,
+ 'coupon_customer_limit' => $customer_limit,
+ 'coupon_type_id' => $type_id,
+ 'coupon_reduction_type' => $reduction_type,
+ 'coupon_reduction_amt' => $reduction_amt,
+ 'coupon_trial_length' => $trial_length,
+ 'coupon_min_cart_amt' => $min_amt,
+ );
+
+ // Add the created_on field
+ $insert_fields['created_on'] = date('Y-m-d H:i:s');
+
+ // Now, time to try saving the coupon itself
+ $this->db->insert('coupons', $insert_fields);
+
+ $id = $this->db->insert_id();
+
+ if (is_numeric($id))
+ {
+ // Save was successfull, so try to save our various associated parts.
+ if (!empty($products)) { $this->save_related($id, 'coupons_products', 'product_id', $products); }
+ if (!empty($plans)) { $this->save_related($id, 'coupons_subscriptions', 'subscription_plan_id', $plans); }
+ if (!empty($ship_rates)) { $this->save_related($id, 'coupons_shipping', 'shipping_id', $ship_rates); }
+
+ return $id;
+ }
+
+ return FALSE;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Updates an existing coupon in the database.
+ *
+ * @param int $coupon_id The id of the coupon to update
+ * @param string $name The coupon name
+ * @param string $code The coupon code
+ * @param string $start_date The first day that the coupon can be used (ie YYYY-MM-DD)
+ * @param string $end_date The last day the coupon can be used
+ * @param int $max_uses The maximum number of times the coupon can be used
+ * @param bool $customer_limit Whether or not to restrict the customer to a single use
+ * @param int $type_id The type of coupon (Price reduction, free trial or free subscription)
+ * @param int $reduction_type Whether percent or fixed amount of reduction. Only applicable for Price Reductions.
+ * @param string $reduction_amt How much to reduce price by. Only applicable for Price Reduction.
+ * @param int $trial_length How many days the free trial will last.
+ * @param string $min_amt The minimum amount in the cart before they qualify for free shipping.
+ * @param array $products An array of product ids to assign this coupon to.
+ * @param array $plans An array of subscription plans to assign this coupon to.
+ * @param array $ship_rates An array of shipping rates to apply this coupon to.
+ *
+ * @return boolean
+ */
+ public function update_coupon($coupon_id, $name, $code, $start_date, $end_date, $max_uses, $customer_limit, $type_id, $reduction_type, $reduction_amt, $trial_length, $min_amt, $products, $plans, $ship_rates)
+ {
+ $insert_fields = array(
+ 'coupon_name' => $name,
+ 'coupon_code' => $code,
+ 'coupon_start_date' => $start_date,
+ 'coupon_end_date' => $end_date,
+ 'coupon_max_uses' => $max_uses,
+ 'coupon_customer_limit' => $customer_limit,
+ 'coupon_type_id' => $type_id,
+ 'coupon_reduction_type' => $reduction_type,
+ 'coupon_reduction_amt' => $reduction_amt,
+ 'coupon_trial_length' => $trial_length,
+ 'coupon_min_cart_amt' => $min_amt,
+ );
+
+ // Add the created_on field
+ $insert_fields['created_on'] = date('Y-m-d H:i:s');
+
+ // Now, time to try saving the coupon itself
+ $this->db->where('coupon_id', $coupon_id);
+ $this->db->update('coupons', $insert_fields);
+
+ if ($this->db->affected_rows())
+ {
+ // Save was successfull, so try to save our various associated parts.
+ $this->save_related($coupon_id, 'coupons_products', 'product_id', $products);
+ $this->save_related($coupon_id, 'coupons_subscriptions', 'subscription_plan_id', $plans);
+ $this->save_related($coupon_id, 'coupons_shipping', 'shipping_id', $ship_rates);
+
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ //--------------------------------------------------------------------
+
+
+ /**
+ * Sets the deleted flag on a coupon (basically, saves it for the 'recycle bin')
+ *
+ * @param int $id The id of the coupon to delete.
+ *
+ * @return bool Whether the coupon is successfully 'deleted' or not.
+ */
+ public function delete_coupon ($id=null)
+ {
+ if (!is_numeric($id))
+ {
+ return FALSE;
+ }
+
+ $this->db->set('coupon_deleted', 1);
+ $this->db->where('coupon_id', $id);
+ return $this->db->update('coupons');
+ }
+
+ //--------------------------------------------------------------------
+
+
+ /**
+ * Saves related items into their pivot tables (like products, plans, etc)
+ * that a coupon would be associated to.
+ *
+ * @param int $coupon_id The id of the coupon this data is related to.
+ * @param string $table The name of the database table to save to.
+ * @param field $field The name of the database table field to save to.
+ * @param array $items The items to save.
+ *
+ * @return void
+ */
+ public function save_related ($coupon_id=null, $table='', $field='', $items=array())
+ {
+ // First, delete any existing entries for this coupon, just in case it's an edit
+ $this->db->where('coupon_id', $coupon_id);
+ $this->db->delete($table);
+
+ if (empty($items)) {
+ // no new links!
+ return TRUE;
+ }
+
+ // Now save the new values
+ foreach ($items as $item)
+ {
+ $this->db->set(array('coupon_id' => $coupon_id, $field => $item));
+ $this->db->insert($table);
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Retrieves related records (in other db tables) to the specified coupon.
+ *
+ * @param id $coupon_id The id of the coupon this data is related to.
+ * @param string $table The name of the database table to pull from.
+ * @param string $field The name of the database table field to retrieve.
+ *
+ * @return array The related object, or FALSE on failure.
+ */
+ public function get_related($coupon_id=null, $table='', $field='')
+ {
+ $this->db->where('coupon_id', $coupon_id);
+ $query = $this->db->get($table);
+
+ if ($query->num_rows())
+ {
+ $items = $query->result_array();
+
+ foreach ($items as $item)
+ {
+ $collection[] = $item[$field];
+ }
+
+ return $collection;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+}
+
+/* End of file coupon_model.php */
+/* Location: ./application/modules/coupons/models/coupon_model.php */
\ No newline at end of file
diff --git a/app/modules/coupons/views/add.php b/app/modules/coupons/views/add.php
new file mode 100644
index 00000000..2e723ba6
--- /dev/null
+++ b/app/modules/coupons/views/add.php
@@ -0,0 +1,15 @@
+=$this->head_assets->javascript('js/form.coupon.js');?>
+
+=$this->load->view(branded_view('cp/header')); ?>
+
+
New Coupon
+
+
+
+= $this->load->view(branded_view('cp/footer')); ?>
\ No newline at end of file
diff --git a/app/modules/coupons/views/coupons.php b/app/modules/coupons/views/coupons.php
new file mode 100644
index 00000000..a1bcadfa
--- /dev/null
+++ b/app/modules/coupons/views/coupons.php
@@ -0,0 +1,25 @@
+= $this->load->view(branded_view('cp/header')); ?>
+
+Manage Coupons
+
+=$this->dataset->table_head();?>
+
+dataset->data)) : ?>
+ dataset->data as $row) :?>
+
+
+ =$row['id'];?>
+ =$row['name'];?>
+ = $row['code'] ?>
+ = date('Y-m-d', strtotime($row['start_date'])) .' - '. date('Y-m-d', strtotime($row['end_date']));?>
+ = $coupon_options[$row['type_id']] ?>
+
+
+
+
+ No coupons match your filters.
+
+
+
+=$this->dataset->table_close();?>
+= $this->load->view(branded_view('cp/footer')); ?>
\ No newline at end of file
diff --git a/app/modules/coupons/views/edit.php b/app/modules/coupons/views/edit.php
new file mode 100644
index 00000000..12d92dcb
--- /dev/null
+++ b/app/modules/coupons/views/edit.php
@@ -0,0 +1,15 @@
+=$this->head_assets->javascript('js/form.coupon.js');?>
+
+=$this->load->view(branded_view('cp/header')); ?>
+
+Edit Coupon
+
+
+
+= $this->load->view(branded_view('cp/footer')); ?>
\ No newline at end of file
diff --git a/app/modules/custom_fields/libraries/fieldtype.php b/app/modules/custom_fields/libraries/fieldtype.php
index 36e28459..bf928013 100644
--- a/app/modules/custom_fields/libraries/fieldtype.php
+++ b/app/modules/custom_fields/libraries/fieldtype.php
@@ -39,6 +39,7 @@ class Fieldtype {
public $type;
public $value = FALSE;
public $label = FALSE;
+ public $readonly = FALSE;
public $name;
public $help = FALSE;
public $placeholder = FALSE;
@@ -372,6 +373,19 @@ public function name ($name) {
return $this;
}
+ /**
+ * Set Readonly
+ *
+ * @param string $readonly
+ *
+ * @return object $fieldtype_object
+ */
+ public function readonly ($readonly) {
+ $this->readonly = $readonly;
+
+ return $this;
+ }
+
/**
* Set Width
*
diff --git a/app/modules/custom_fields/libraries/fieldtypes/text.php b/app/modules/custom_fields/libraries/fieldtypes/text.php
index 2f2d5b14..66bf3580 100644
--- a/app/modules/custom_fields/libraries/fieldtypes/text.php
+++ b/app/modules/custom_fields/libraries/fieldtypes/text.php
@@ -62,7 +62,11 @@ function output_shared () {
'placeholder' => $this->placeholder,
'style' => 'width: ' . $this->width,
'class' => implode(' ', $this->field_classes)
- );
+ );
+
+ if(!empty($this->readonly)){
+ $attributes['readonly'] = $this->readonly;
+ }
// compile attributes
$attributes = $this->compile_attributes($attributes);
@@ -86,6 +90,10 @@ function output_admin () {
$help = ($this->help == FALSE) ? '' : '' . $this->help . '';
+ if($this->readonly !== FALSE){
+ $attributes .= $this->readonly;
+ }
+
// build HTML
$return = '
diff --git a/app/modules/disqus/controllers/admincp.php b/app/modules/disqus/controllers/admincp.php
new file mode 100644
index 00000000..639584d7
--- /dev/null
+++ b/app/modules/disqus/controllers/admincp.php
@@ -0,0 +1,50 @@
+admin_navigation->parent_active('configuration');
+ }
+
+ function index() {
+ $this->load->library('custom_fields/form_builder');
+ $shortname = $this->form_builder->add_field('text')
+ ->name('disqus_shortname')
+ ->label('Disqus Shortname')
+ ->validators(array('alpha_numeric','trim'))
+ ->value(setting('disqus_shortname'))
+ ->help('Don\'t have a shortname? Register your site at Disqus.')
+ ->required(TRUE);
+
+ $data = array(
+ 'form_title' => 'Disqus Configuration',
+ 'form_action' => site_url('admincp/disqus/post_config'),
+ 'form' => $this->form_builder->output_admin(),
+ 'form_button' => 'Save Configuration',
+ 'disqus_shortname' => setting('disqus_shortname')
+ );
+
+ $this->load->view('generic', $data);
+ }
+
+ function post_config () {
+ $this->settings_model->update_setting('disqus_shortname',$this->input->post('disqus_shortname'));
+
+ $this->notices->SetNotice('Disqus configuration saved.');
+
+ redirect('admincp/disqus');
+ }
+}
\ No newline at end of file
diff --git a/app/modules/disqus/disqus.php b/app/modules/disqus/disqus.php
new file mode 100644
index 00000000..afc34cd7
--- /dev/null
+++ b/app/modules/disqus/disqus.php
@@ -0,0 +1,60 @@
+active_module = $this->name;
+
+ parent::__construct();
+ }
+
+ /*
+ * Pre-admin function
+ *
+ * Initiate navigation in control panel
+ */
+ function admin_preload ()
+ {
+ $this->CI->admin_navigation->child_link('configuration',65,'Disqus Comments',site_url('admincp/disqus'));
+ }
+
+ /**
+ * Pre-front Method
+ *
+ * Triggered prior to loading the frontend
+ */
+ function front_preload () {
+ $this->CI->smarty->addPluginsDir(APPPATH . 'modules/disqus/template_plugins/');
+ }
+
+ /*
+ * Module update
+ *
+ * @param int $db_version The current DB version
+ *
+ * @return int The current software version, to update the database
+ */
+ function update ($db_version) {
+ if ($db_version < '1.0') {
+ // initial install
+ $this->CI->settings_model->new_setting(1, 'disqus_shortname', '', '', 'text','', FALSE, TRUE);
+ }
+
+ // return current version
+ return $this->version;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/disqus/template_plugins/function.disqus_comments.php b/app/modules/disqus/template_plugins/function.disqus_comments.php
new file mode 100644
index 00000000..ea32b3d3
--- /dev/null
+++ b/app/modules/disqus/template_plugins/function.disqus_comments.php
@@ -0,0 +1,24 @@
+ Disqus Comments in the control panel.');
+ }
+
+ return "
+
+
+";
+}
\ No newline at end of file
diff --git a/app/modules/disqus/views/generic.php b/app/modules/disqus/views/generic.php
new file mode 100644
index 00000000..8867f428
--- /dev/null
+++ b/app/modules/disqus/views/generic.php
@@ -0,0 +1,26 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+
+ if (!empty($disqus_shortname)) { ?>
+
+Your Disqus comments section is configured properly.
+
+Paste the following code in your templates where you would like to add a comments section.
+
+
+{disqus_comments}
+
+
+ } ?>
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/error/controllers/error.php b/app/modules/error/controllers/error.php
new file mode 100644
index 00000000..1b4778bf
--- /dev/null
+++ b/app/modules/error/controllers/error.php
@@ -0,0 +1,31 @@
+output->set_status_header('404');
+ $content = array(
+ 'title' => '404 Page Not Found'
+ ,'message' => 'The page you requested was not found.'
+ );
+
+ // show content
+ $this->smarty->assign($content);
+
+ return $this->smarty->display('error.thtml');
+ }
+}
\ No newline at end of file
diff --git a/app/modules/forms/models/form_model.php b/app/modules/forms/models/form_model.php
index 2e14e095..d27678e7 100644
--- a/app/modules/forms/models/form_model.php
+++ b/app/modules/forms/models/form_model.php
@@ -279,8 +279,8 @@ function new_response($form_id, $user_id = FALSE, $custom_fields = array()) {
if (empty($form)) {
die(show_error('Invalid form ID.'));
}
-
- $date = date('Y-m-d H:i:s');
+ $time = time();
+ $date = date('Y-m-d H:i:s', $time);
$insert_fields = array(
'submission_date' => $date,
@@ -302,7 +302,7 @@ function new_response($form_id, $user_id = FALSE, $custom_fields = array()) {
// build body
$lines = array();
- $lines[] = 'Date: ' . date('F j, Y, g:i a', strtotime($date));
+ $lines[] = 'Date: ' . date('F j, Y, g:i a T', $time);
if (!empty($user_id)) {
$user = $this->user_model->get_user($user_id);
@@ -310,9 +310,6 @@ function new_response($form_id, $user_id = FALSE, $custom_fields = array()) {
$lines[] = 'Member Name: ' . $user['first_name'] . ' ' . $user['last_name'];
$lines[] = 'Member Email: ' . $user['email'];
}
- else {
- $lines[] = 'Member: None';
- }
foreach ($form['custom_fields'] as $field) {
if (@is_array(unserialize($custom_fields[$field['name']]))) {
diff --git a/app/modules/gallery/controllers/admincp.php b/app/modules/gallery/controllers/admincp.php
new file mode 100644
index 00000000..c97ff927
--- /dev/null
+++ b/app/modules/gallery/controllers/admincp.php
@@ -0,0 +1,537 @@
+admin_navigation->parent_active('publish');
+ }
+
+
+ function index () {
+ // get type_id from settings table
+ $type_id = $this->config->item('gallery_content_type_id');
+
+ $this->admin_navigation->module_link('Publish New Gallery',site_url('admincp/gallery/create/' . $type_id));
+
+ $this->load->library('dataset');
+
+ $columns = array(
+ array(
+ 'name' => 'ID #',
+ 'type' => 'id',
+ 'width' => '5%',
+ 'filter' => 'text'
+ ),
+ array(
+ 'name' => 'Title',
+ 'width' => '30%',
+ 'filter' => 'title',
+ 'type' => 'text',
+ 'sort_column' => 'content.content_title'
+ ),
+ array(
+ 'name' => 'Author',
+ 'width' => '15%',
+ 'filter' => 'author_like',
+ 'type' => 'text'
+ ),
+ array(
+ 'name' => 'Date',
+ 'width' => '20%',
+ 'sort_column' => 'content.content_date',
+ 'type' => 'date',
+ 'filter' => 'date',
+ 'field_start_date' => 'start_date',
+ 'field_end_date' => 'end_date'
+ ),
+ array(
+ 'name' => 'Hits',
+ 'width' => '10%',
+ 'sort_column' => 'content.content_hits'
+ ),
+ array(
+ 'name' => '',
+ 'width' => '20%'
+ )
+ );
+
+ $this->dataset->columns($columns);
+ $this->dataset->datasource('publish/content_model','get_contents', array('allow_future' => TRUE, 'type' => $type_id));
+ $this->dataset->base_url(site_url('admincp/gallery'));
+
+ // initialize the dataset
+ $this->dataset->initialize(FALSE);
+
+ // count total rows
+ $this->load->model('publish/content_model');
+ $total_rows = $this->content_model->count_content($this->dataset->get_unlimited_parameters());
+ $this->dataset->total_rows($total_rows);
+ $this->dataset->initialize_pagination();
+
+ // add actions
+ $this->dataset->action('Delete','admincp/gallery/delete');
+
+ $this->load->view('galleries');
+ }
+
+ function delete ($contents, $return_url) {
+ $this->load->library('asciihex');
+ $this->load->model('publish/content_model');
+
+ $contents = unserialize(base64_decode($this->asciihex->HexToAscii($contents)));
+ $return_url = base64_decode($this->asciihex->HexToAscii($return_url));
+
+ foreach ($contents as $content) {
+ $this->content_model->delete_content($content);
+ }
+
+ $this->notices->SetNotice('Galleries deleted successfully.');
+
+ redirect($return_url);
+
+ return TRUE;
+ }
+
+ function create () {
+ $type = $this->config->item('gallery_content_type_id');
+
+ $this->load->model('publish/content_type_model');
+ $type = $this->content_type_model->get_content_type($type);
+
+ $this->load->library('admin_form');
+
+ $title = new Admin_form;
+ $title->fieldset('Standard Page Elements');
+ $title->text('Title','title','',FALSE,TRUE,FALSE,TRUE);
+ $title->hidden('base_url',$type['base_url']);
+ $title->text('URL Path','url_path',$type['base_url'],'If you leave this blank, it will be auto-generated from the Title above.',FALSE,'e.g., /about/contact_us',FALSE,'500px');
+
+ // we will build the rest of the sidebar form with form_builder because we want to use it's cool
+ // fieldtypes and better API
+ $this->load->model('publish/topic_model');
+ $topics = $this->topic_model->get_tiered_topics();
+
+ $options = array();
+ foreach ($topics as $data) {
+ $options[] = array('name' => $data['name'], 'value' => $data['id']);
+ }
+
+ $this->load->library('custom_fields/form_builder');
+ $topics = $this->form_builder->add_field('multicheckbox');
+ $topics->options($options)
+ ->name('topics')
+ ->label('Topics');
+
+ $date = $this->form_builder->add_field('datetime');
+ $date->data('future_only',TRUE)
+ ->name('date')
+ ->label('Publish Date')
+ ->value(date('Y-m-d H:i:s'));
+
+ $title = $title->display();
+ $standard = $this->form_builder->output_admin();
+
+ // we require a member group access privileges dropdown
+ $this->load->model('users/usergroup_model');
+ $groups = $this->usergroup_model->get_usergroups();
+
+ $options = array();
+ $options[] = array('name' => 'Public / Any Member Group', 'value' => '0');
+ foreach ($groups as $group) {
+ $options[] = array('name' => $group['name'], 'value' => $group['id']);
+ }
+
+ $this->load->library('custom_fields/form_builder');
+ $this->form_builder->reset();
+ $privileges = $this->form_builder->add_field('multicheckbox');
+ $privileges->name('privileges')
+ ->options($options)
+ ->default_value(0)
+ ->label('Allowed Membership Groups')
+ ->help('If a group or groups is selected, this content will require the user be in this group to view it. This enables you to
+ charge for subscriptions and products that move the user to this group.');
+
+ $privileges = $this->form_builder->output_admin();
+
+ // handle custom fields
+ $this->load->model('custom_fields_model');
+ $custom_fieldset = new Admin_form;
+ $custom_fields = $this->custom_fields_model->get_custom_fields(array('group' => $type['custom_field_group_id']));
+ $custom_fieldset->fieldset('Custom Fields');
+ $custom_fieldset->custom_fields($custom_fields);
+ $custom_fields = $custom_fieldset->display();
+
+ $data = array(
+ 'title' => $title,
+ 'standard' => $standard,
+ 'privileges' => $privileges,
+ 'custom_fields' => $custom_fields,
+ 'type' => $type,
+ 'invalid' => FALSE,
+ 'form_title' => 'Publish New Gallery',
+ 'form_action' => site_url('admincp/gallery/post/new')
+ );
+
+ $this->load->view('create', $data);
+ }
+
+ function edit ($id) {
+ $this->load->model('publish/content_model');
+ $content = $this->content_model->get_content($id, TRUE);
+
+ $this->load->model('publish/content_type_model');
+ $type = $this->content_type_model->get_content_type($content['type_id']);
+
+ $this->load->library('admin_form');
+
+ $title = new Admin_form;
+ $title->fieldset('Standard Page Elements');
+ $title->text('Title','title',$content['title'],FALSE,TRUE,FALSE,TRUE);
+
+ // if we are using the base_url in the current URL, chances are we want to keep it for future URL's
+ if (isset($type['base_url']) and !empty($type['base_url']) and strpos($content['url_path'], $type['base_url']) === 0) {
+ $title->hidden('base_url',$type['base_url']);
+ }
+
+ $title->text('URL Path','url_path',$content['url_path'],'If you leave this blank, it will be auto-generated from the Title above.',FALSE,'e.g., /about/contact_us',FALSE,'500px');
+
+ // we will build the rest of the sidebar form with form_builder because we want to use it's cool
+ // fieldtypes and better API
+ $this->load->model('publish/topic_model');
+ $topics = $this->topic_model->get_tiered_topics();
+
+ $options = array();
+ foreach ($topics as $data) {
+ $options[] = array('name' => $data['name'], 'value' => $data['id']);
+ }
+
+ $this->load->library('custom_fields/form_builder');
+ $topics = $this->form_builder->add_field('multicheckbox');
+ $topics->options($options)
+ ->name('topics')
+ ->label('Topics');
+
+ $date = $this->form_builder->add_field('datetime');
+ $date->data('future_only',TRUE)
+ ->name('date')
+ ->label('Publish Date');
+
+ // editing, assign values
+ $topics->value($content['topics']);
+ $date->value($content['date']);
+
+ $title = $title->display();
+ $standard = $this->form_builder->output_admin();
+
+ // we require a member group access privileges dropdown
+ $this->load->model('users/usergroup_model');
+ $groups = $this->usergroup_model->get_usergroups();
+
+ $options = array();
+ $options[] = array('name' => 'Public / Any Member Group', 'value' => '0');
+ foreach ($groups as $group) {
+ $options[] = array('name' => $group['name'], 'value' => $group['id']);
+ }
+
+ $this->load->library('custom_fields/form_builder');
+ $this->form_builder->reset();
+ $privileges = $this->form_builder->add_field('multicheckbox');
+ $privileges->name('privileges')
+ ->options($options)
+ ->default_value(0)
+ ->label('Allowed Membership Groups')
+ ->help('If a group or groups is selected, this content will require the user be in this group to view it. This enables you to
+ charge for subscriptions and products that move the user to this group.')
+ ->value($content['privileges']);
+
+ $privileges = $this->form_builder->output_admin();
+
+ // handle custom fields
+ $this->load->model('custom_fields_model');
+ $custom_fieldset = new Admin_form;
+ $custom_fields = $this->custom_fields_model->get_custom_fields(array('group' => $type['custom_field_group_id']));
+ $custom_fieldset->fieldset('Custom Product Data');
+ $custom_fieldset->custom_fields($custom_fields, $content);
+ $custom_fields = $custom_fieldset->display();
+
+ $data = array(
+ 'title' => $title,
+ 'standard' => $standard,
+ 'privileges' => $privileges,
+ 'custom_fields' => $custom_fields,
+ 'type' => $type,
+ 'form_title' => 'Edit Gallery',
+ 'form_action' => site_url('admincp/gallery/post/edit/' . $content['id']),
+ 'invalid' => ($this->input->get('invalid')) ? TRUE : FALSE,
+ 'errors' => $this->session->flashdata('errors')
+ );
+
+ $this->load->view('create', $data);
+ }
+
+ function post ($action = 'new', $id = FALSE) {
+ $this->load->model('publish/content_type_model');
+ $type = $this->content_type_model->get_content_type($this->input->post('type'));
+
+ $this->load->model('publish/content_model');
+
+ // get values for topics/publish date if standard
+ $this->load->library('custom_fields/form_builder');
+ $this->form_builder->reset();
+ $this->form_builder->add_field('multicheckbox')->name('topics')->label('Topics');
+ $this->form_builder->add_field('datetime')->name('date')->label('Publish Date')->default_value(date('Y-m-d H:i:s'));
+
+ $form_builder_data = $this->form_builder->post_to_array();
+
+ $topics = unserialize($form_builder_data['topics']);
+ $date = $form_builder_data['date'];
+
+ $this->load->library('custom_fields/form_builder');
+ $this->form_builder->reset();
+ $this->form_builder->add_field('multicheckbox')->name('privileges')->label('Member Access Groups');
+
+ $form_builder_data = $this->form_builder->post_to_array();
+
+ $privileges = unserialize($form_builder_data['privileges']);
+
+ // gather custom field data
+ $this->load->library('custom_fields/form_builder');
+ $this->form_builder->build_form_from_group($type['custom_field_group_id']);
+
+ // validation, though we won't kill script if it doesn't validate because we don't
+ // want to lose any data
+ // we'll allow the post to save, then take them to the edit screen with errors!
+ if ($this->form_builder->validate_post() === FALSE) {
+ $validation_errors = $this->form_builder->validation_errors();
+ $this->notices->SetError($validation_errors);
+ $error = TRUE;
+ }
+ else {
+ $error = FALSE;
+ }
+
+ $custom_fields = $this->form_builder->post_to_array();
+
+ if ($action == 'new') {
+ $content_id = $this->content_model->new_content(
+ $this->input->post('type'),
+ $this->user_model->get('id'),
+ $this->input->post('title'),
+ $this->input->post('url_path'),
+ $topics,
+ $privileges,
+ $date,
+ $custom_fields
+ );
+
+ // we're going to re-route this to our own controller
+ $content = $this->content_model->get_content($content_id, TRUE);
+
+ $this->db->update('links', array(
+ 'link_module' => 'gallery',
+ 'link_controller' => 'gallery',
+ 'link_method' => 'view'
+ ),
+ array('link_id' => $content['link_id'])
+ );
+
+ // re-generate routes file
+ $this->load->model('link_model');
+ $this->link_model->gen_routes_file();
+
+ if ($error == FALSE) {
+ $this->notices->SetNotice('Gallery posted successfully.');
+ }
+ }
+ elseif ($action == 'edit') {
+ $this->content_model->update_content(
+ $id,
+ $this->input->post('title'),
+ $this->input->post('url_path'),
+ $topics,
+ $privileges,
+ $date,
+ $custom_fields
+ );
+
+ if ($error == FALSE) {
+ $this->notices->SetNotice('Gallery updated successfully.');
+ }
+ }
+
+ if ($error == TRUE) {
+ // may not have $content_id if editign
+ if (!empty($id)) {
+ $content_id = $id;
+ }
+
+ $this->session->set_flashdata('errors', $validation_errors);
+ redirect('admincp/gallery/edit/' . $content_id . '?invalid=TRUE');
+ }
+ else {
+ redirect('admincp/gallery');
+ }
+ }
+
+ function images ($id) {
+ $this->admin_navigation->module_link('Manage Galleries',site_url('admincp/gallery'));
+ $this->admin_navigation->module_link('Edit this Gallery',site_url('admincp/gallery/edit/' . $id));
+
+ $this->load->model('publish/content_model');
+ $gallery = $this->content_model->get_content($id);
+
+ // get images
+ $this->load->model('gallery/gallery_image_model');
+ $gallery['images'] = $this->gallery_image_model->get_images($gallery['id']);
+
+ // gallery
+ $this->load->library('image_gallery_form');
+ $image_gallery = new Image_gallery_form;
+ //$gallery->label('Upload New Images');
+ //$gallery->name('product_images');
+ $image_gallery->show_upload_button(FALSE);
+
+ $this->load->helper('format_size');
+ $this->load->helper('image_thumb');
+
+ $data = array(
+ 'form_action' => site_url('admincp/gallery/post_images/' . $gallery['id']),
+ 'gallery' => $gallery,
+ 'images' => $image_gallery->display()
+ );
+
+ $this->load->view('gallery', $data);
+ }
+
+ function save_image_order ($content_id) {
+ $this->load->model('gallery/gallery_image_model');
+
+ // reset
+ $this->gallery_image_model->images_reset_order($content_id);
+
+ $count = 1;
+ foreach ($_POST['image'] as $image_id) {
+ $this->gallery_image_model->image_update_order($image_id, $count);
+ $count++;
+ }
+ }
+
+ function image_feature ($content_id, $image_id) {
+ $this->load->model('gallery/gallery_image_model');
+ $this->gallery_image_model->make_feature_image($image_id);
+
+ redirect('admincp/gallery/images/' . $content_id);
+ }
+
+ function image_delete ($content_id, $image_id) {
+ $this->load->model('gallery/gallery_image_model');
+ $this->gallery_image_model->remove_image($image_id);
+
+ redirect('admincp/gallery/images/' . $content_id);
+ }
+
+ function post_images ($content_id) {
+ // deal with image uploads
+ $config = array();
+ $config['upload_path'] = setting('path_writeable') . 'gallery_images/';
+ $config['allowed_types'] = 'zip|jpg|gif|png';
+
+ // upload class may already be loaded
+ $this->load->library('upload');
+
+ // do we already have images
+ $this->load->model('gallery_image_model');
+ $images = $this->gallery_image_model->get_images($content_id);
+
+ if (isset($images[0]['featured']) and $images[0]['featured'] == '1') {
+ $has_feature = TRUE;
+ }
+ else {
+ $has_feature = FALSE;
+ }
+
+ if (is_uploaded_file($_FILES['image']['tmp_name'])) {
+ // get extension
+ $this->load->helper('file_extension');
+ $ext = file_extension($_FILES['image']['name']);
+
+ // set random filename
+ if ($ext == 'zip') {
+ $file_name = 'zip' . time() . '.zip';
+ }
+ else {
+ $file_name = 'img' . time() . '.' . $ext;
+ }
+
+ $this->upload->initialize($config);
+
+ if (!$this->upload->do_upload('image')) {
+ die(show_error($this->upload->display_errors()));
+ }
+
+ // rename to filename
+ rename(setting('path_writeable') . 'gallery_images/' . $this->upload->file_name, setting('path_writeable') . 'gallery_images/' . $file_name);
+
+ // zip archive?
+ if ($ext == 'zip') {
+ $this->load->library('unzip');
+ // only take out these files, anything else is ignored
+ $this->unzip->allow(array('png', 'gif', 'jpeg', 'jpg'));
+
+ $folder_name = substr_replace($file_name, '', -4, 4);
+ $full_path = setting('path_writeable') . 'gallery_images/' . $folder_name;
+ $this->settings_model->make_writeable_folder($full_path, TRUE);
+ $this->unzip->extract(setting('path_writeable') . 'gallery_images/' . $file_name, $full_path);
+
+ $this->load->helper('directory');
+ $files = directory_map($full_path, 1);
+
+ $count = 1;
+ $time = time();
+ foreach ($files as $file) {
+ $ext = file_extension($file);
+
+ // ignore the filler index.html file
+ if ($ext != 'html') {
+ $this_filename = 'img' . $time . '_' . $count . '.' . $ext;
+
+ copy($full_path . '/' . $file, setting('path_writeable') . 'gallery_images/' . $this_filename);
+
+ $image_id = $this->gallery_image_model->add_image($content_id, $this_filename);
+
+ $count++;
+ }
+ }
+ }
+ else {
+ // single image
+ $image_id = $this->gallery_image_model->add_image($content_id, $file_name);
+ }
+
+ if ($has_feature == FALSE) {
+ $this->gallery_image_model->make_feature_image($image_id);
+ $has_feature = TRUE;
+ }
+
+ $this->notices->SetNotice('Image(s) uploaded successfully.');
+ }
+ else {
+ $this->notices->SetNotice('No file uploaded.');
+ }
+
+ return redirect('admincp/gallery/images/' . $content_id);
+ }
+}
\ No newline at end of file
diff --git a/app/modules/gallery/controllers/gallery.php b/app/modules/gallery/controllers/gallery.php
new file mode 100644
index 00000000..1d816fbc
--- /dev/null
+++ b/app/modules/gallery/controllers/gallery.php
@@ -0,0 +1,80 @@
+load->model('publish/content_model');
+
+ $content_id = $this->content_model->get_content_id($url_path);
+
+ if (empty($content_id)) {
+ return show_404($url_path);
+ }
+
+ $content = $this->content_model->get_content($content_id);
+
+ // does this content exist?
+ if (empty($content)) {
+ return show_404($url_path);
+ }
+
+ // load images into gallery
+ $this->load->model('gallery/gallery_image_model');
+ $content['images'] = $this->gallery_image_model->get_images($content['id']);
+
+ // do they have permissions to see content?
+ if (!$this->user_model->in_group($content['privileges'])) {
+ $this->load->helper('paywall/paywall');
+ if (paywall($content, 'content') !== FALSE) {
+ die();
+ }
+ }
+
+ // are we downloading as a zip?
+ if ($this->input->get('download') == 'zip') {
+ return $this->download_zip($content);
+ }
+
+ // show content
+ $this->smarty->assign($content);
+ return $this->smarty->display('gallery.thtml');
+ }
+
+ /**
+ * Download as Zip
+ *
+ * @param array $content The full content array, including "images" key
+ *
+ * @return download file
+ */
+ function download_zip ($content = array()) {
+ $this->load->library('zip');
+
+ foreach ($content['images'] as $image) {
+ $this->zip->read_file($image['path']);
+ }
+
+ // Download the file to your desktop. Name it "my_backup.zip"
+ return $this->zip->download($content['url_path'] . '.zip');
+ }
+}
\ No newline at end of file
diff --git a/app/modules/gallery/gallery.php b/app/modules/gallery/gallery.php
new file mode 100644
index 00000000..fc70288d
--- /dev/null
+++ b/app/modules/gallery/gallery.php
@@ -0,0 +1,82 @@
+active_module = $this->name;
+
+ parent::__construct();
+ }
+
+ /*
+ * Pre-admin function
+ *
+ * Initiate navigation in control panel
+ */
+ function admin_preload ()
+ {
+ $this->CI->admin_navigation->child_link('publish',29,'• Manage Galleries',site_url('admincp/gallery'));
+ }
+
+ function update ($db_version) {
+ if ($db_version < 1.0) {
+ $this->CI->db->query('CREATE TABLE `gallery_images` (
+ `gallery_image_id` int(11) NOT NULL auto_increment,
+ `content_id` int(11) NOT NULL,
+ `gallery_image_filename` varchar(250) NOT NULL,
+ `gallery_image_featured` tinyint(1) NOT NULL,
+ `gallery_image_order` int(5) NOT NULL,
+ `gallery_image_uploaded` DATETIME,
+ `gallery_title` varchar(255) NOT NULL,
+ `gallery_caption` text,
+ PRIMARY KEY (`gallery_image_id`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;');
+
+ // create gallery content type
+ $this->CI->load->model('publish/content_type_model');
+ $gallery_content_type_id = $this->CI->content_type_model->new_content_type('Galleries', TRUE, TRUE, FALSE, 'gallery.thtml');
+
+ $this->CI->settings_model->new_setting(1, 'gallery_content_type_id', $gallery_content_type_id, '', 'text', '', FALSE, TRUE);
+
+ // create "description" custom field
+ $content_type = $this->CI->content_type_model->get_content_type($gallery_content_type_id);
+
+ $this->CI->load->model('custom_fields_model');
+ $this->CI->custom_fields_model->new_custom_field($content_type['custom_field_group_id'], 'Description', 'wysiwyg', FALSE, FALSE, '650px', FALSE, TRUE, FALSE, $content_type['system_name']);
+ }
+
+ if ($db_version < 1.01) {
+ $path = $this->CI->config->item('path_writeable') . 'gallery_images';
+ $this->CI->settings_model->make_writeable_folder($path);
+ }
+
+ if ($db_version < 1.02) {
+ $this->CI->db->update('content_types', array('content_type_is_module' => '1'), array('content_type_id' => $this->CI->config->item('gallery_content_type_id')));
+ }
+
+ if ($db_version < 1.03) {
+ $content_type = $this->CI->db->where('content_type_system_name','galleries')
+ ->get('content_types')
+ ->row_array();
+
+ $this->CI->load->model('custom_fields_model');
+ $this->CI->custom_fields_model->new_custom_field($content_type['custom_field_group_id'], 'Feature Image', 'text', FALSE, FALSE, '650px', 'Leave blank - this will be set automatically', FALSE, FALSE, $content_type['content_type_system_name']);
+ }
+
+ return $this->version;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/gallery/libraries/unzip.php b/app/modules/gallery/libraries/unzip.php
new file mode 100644
index 00000000..aee41156
--- /dev/null
+++ b/app/modules/gallery/libraries/unzip.php
@@ -0,0 +1,622 @@
+_zip_file = $zip_file;
+ $this->_target_dir = $target_dir ? $target_dir : dirname($this->_zip_file);
+
+ if ( ! $files = $this->_list_files())
+ {
+ $this->set_error('ZIP folder was empty.');
+ return FALSE;
+ }
+
+ $file_locations = array();
+ foreach ($files as $file => $trash)
+ {
+ $dirname = pathinfo($file, PATHINFO_DIRNAME);
+ $extension = pathinfo($file, PATHINFO_EXTENSION);
+
+ $folders = explode('/', $dirname);
+ $out_dn = $this->_target_dir . '/' . $dirname;
+
+ // Skip stuff in stupid folders
+ if (in_array(current($folders), $this->_skip_dirs))
+ {
+ continue;
+ }
+
+ // Skip any files that are not allowed
+ if (is_array($this->_allow_extensions) AND $extension AND ! in_array($extension, $this->_allow_extensions))
+ {
+ continue;
+ }
+
+ if ( ! is_dir($out_dn) AND $preserve_filepath)
+ {
+ $str = "";
+ foreach ($folders as $folder)
+ {
+ $str = $str ? $str . '/' . $folder : $folder;
+ if ( ! is_dir($this->_target_dir . '/' . $str))
+ {
+ $this->set_debug('Creating folder: ' . $this->_target_dir . '/' . $str);
+
+ if ( ! @mkdir($this->_target_dir . '/' . $str))
+ {
+ $this->set_error('Desitnation path is not writable.');
+ return FALSE;
+ }
+
+ // Apply chmod if configured to do so
+ $this->apply_chmod AND chmod($this->_target_dir . '/' . $str, $this->apply_chmod);
+ }
+ }
+ }
+
+ if (substr($file, -1, 1) == '/') continue;
+
+ $file_locations[] = $file_location = $this->_target_dir . '/' . ($preserve_filepath ? $file : basename($file));
+
+ $this->_extract_file($file, $file_location);
+ }
+
+ return $file_locations;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * What extensions do we want out of this ZIP
+ *
+ * @access Public
+ * @param none
+ * @return none
+ */
+ public function allow($ext = NULL)
+ {
+ $this->_allow_extensions = $ext;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Show error messages
+ *
+ * @access public
+ * @param string
+ * @return string
+ */
+ public function error_string($open = '', $close = '
')
+ {
+ return $open . implode($close . $open, $this->error) . $close;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Show debug messages
+ *
+ * @access public
+ * @param string
+ * @return string
+ */
+ public function debug_string($open = '', $close = '
')
+ {
+ return $open . implode($close . $open, $this->info) . $close;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Save errors
+ *
+ * @access Private
+ * @param string
+ * @return none
+ */
+ function set_error($string)
+ {
+ $this->error[] = $string;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Save debug data
+ *
+ * @access Private
+ * @param string
+ * @return none
+ */
+ function set_debug($string)
+ {
+ $this->info[] = $string;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * List all files in archive.
+ *
+ * @access Public
+ * @param boolean
+ * @return mixed
+ */
+ private function _list_files($stop_on_file = FALSE)
+ {
+ if (sizeof($this->compressed_list))
+ {
+ $this->set_debug('Returning already loaded file list.');
+ return $this->compressed_list;
+ }
+
+ // Open file, and set file handler
+ $fh = fopen($this->_zip_file, 'r');
+ $this->fh = &$fh;
+
+ if ( ! $fh)
+ {
+ $this->set_error('Failed to load file: ' . $this->_zip_file);
+ return FALSE;
+ }
+
+ $this->set_debug('Loading list from "End of Central Dir" index list...');
+
+ if ( ! $this->_load_file_list_by_eof($fh, $stop_on_file))
+ {
+ $this->set_debug('Failed! Trying to load list looking for signatures...');
+
+ if ( ! $this->_load_files_by_signatures($fh, $stop_on_file))
+ {
+ $this->set_debug('Failed! Could not find any valid header.');
+ $this->set_error('ZIP File is corrupted or empty');
+
+ return FALSE;
+ }
+ }
+
+ return $this->compressed_list;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Unzip file in archive.
+ *
+ * @access Public
+ * @param string, boolean
+ * @return Unziped file.
+ */
+ private function _extract_file($compressed_file_name, $target_file_name = FALSE)
+ {
+ if ( ! sizeof($this->compressed_list))
+ {
+ $this->set_debug('Trying to unzip before loading file list... Loading it!');
+ $this->_list_files(FALSE, $compressed_file_name);
+ }
+
+ $fdetails = &$this->compressed_list[$compressed_file_name];
+
+ if ( ! isset($this->compressed_list[$compressed_file_name]))
+ {
+ $this->set_error('File "$compressed_file_name" is not compressed in the zip.');
+ return FALSE;
+ }
+
+ if (substr($compressed_file_name, -1) == '/')
+ {
+ $this->set_error('Trying to unzip a folder name "$compressed_file_name".');
+ return FALSE;
+ }
+
+ if ( ! $fdetails['uncompressed_size'])
+ {
+ $this->set_debug('File "$compressed_file_name" is empty.');
+
+ return $target_file_name ? file_put_contents($target_file_name, '') : '';
+ }
+
+ fseek($this->fh, $fdetails['contents_start_offset']);
+ $ret = $this->_uncompress(
+ fread($this->fh, $fdetails['compressed_size']),
+ $fdetails['compression_method'],
+ $fdetails['uncompressed_size'],
+ $target_file_name
+ );
+
+ if ($this->apply_chmod AND $target_file_name)
+ {
+ chmod($target_file_name, FILE_READ_MODE);
+ }
+
+ return $ret;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Free the file resource.
+ *
+ * @access Public
+ * @param none
+ * @return none
+ */
+ public function close()
+ {
+ // Free the file resource
+ if ($this->fh)
+ {
+ fclose($this->fh);
+ }
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Free the file resource Automatic destroy.
+ *
+ * @access Public
+ * @param none
+ * @return none
+ */
+ public function __destroy()
+ {
+ $this->close();
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Uncompress file. And save it to the targetFile.
+ *
+ * @access Private
+ * @param Filecontent, int, int, boolean
+ * @return none
+ */
+ private function _uncompress($content, $mode, $uncompressed_size, $target_file_name = FALSE)
+ {
+ switch ($mode)
+ {
+ case 0:
+ return $target_file_name ? file_put_contents($target_file_name, $content) : $content;
+ case 1:
+ $this->set_error('Shrunk mode is not supported... yet?');
+ return FALSE;
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ $this->set_error('Compression factor ' . ($mode - 1) . ' is not supported... yet?');
+ return FALSE;
+ case 6:
+ $this->set_error('Implode is not supported... yet?');
+ return FALSE;
+ case 7:
+ $this->set_error('Tokenizing compression algorithm is not supported... yet?');
+ return FALSE;
+ case 8:
+ // Deflate
+ return $target_file_name ?
+ file_put_contents($target_file_name, gzinflate($content, $uncompressed_size)) :
+ gzinflate($content, $uncompressed_size);
+ case 9:
+ $this->set_error('Enhanced Deflating is not supported... yet?');
+ return FALSE;
+ case 10:
+ $this->set_error('PKWARE Date Compression Library Impoloding is not supported... yet?');
+ return FALSE;
+ case 12:
+ // Bzip2
+ return $target_file_name ?
+ file_put_contents($target_file_name, bzdecompress($content)) :
+ bzdecompress($content);
+ case 18:
+ $this->set_error('IBM TERSE is not supported... yet?');
+ return FALSE;
+ default:
+ $this->set_error('Unknown uncompress method: $mode');
+ return FALSE;
+ }
+ }
+
+ private function _load_file_list_by_eof(&$fh, $stop_on_file = FALSE)
+ {
+ // Check if there's a valid Central Dir signature.
+ // Let's consider a file comment smaller than 1024 characters...
+ // Actually, it length can be 65536.. But we're not going to support it.
+
+ for ($x = 0; $x < 1024; $x++)
+ {
+ fseek($fh, -22 - $x, SEEK_END);
+
+ $signature = fread($fh, 4);
+
+ if ($signature == $this->central_signature_end)
+ {
+ // If found EOF Central Dir
+ $eodir['disk_number_this'] = unpack("v", fread($fh, 2)); // number of this disk
+ $eodir['disk_number'] = unpack("v", fread($fh, 2)); // number of the disk with the start of the central directory
+ $eodir['total_entries_this'] = unpack("v", fread($fh, 2)); // total number of entries in the central dir on this disk
+ $eodir['total_entries'] = unpack("v", fread($fh, 2)); // total number of entries in
+ $eodir['size_of_cd'] = unpack("V", fread($fh, 4)); // size of the central directory
+ $eodir['offset_start_cd'] = unpack("V", fread($fh, 4)); // offset of start of central directory with respect to the starting disk number
+ $zip_comment_lenght = unpack("v", fread($fh, 2)); // zipfile comment length
+ $eodir['zipfile_comment'] = $zip_comment_lenght[1] ? fread($fh, $zip_comment_lenght[1]) : ''; // zipfile comment
+
+ $this->end_of_central = array(
+ 'disk_number_this' => $eodir['disk_number_this'][1],
+ 'disk_number' => $eodir['disk_number'][1],
+ 'total_entries_this' => $eodir['total_entries_this'][1],
+ 'total_entries' => $eodir['total_entries'][1],
+ 'size_of_cd' => $eodir['size_of_cd'][1],
+ 'offset_start_cd' => $eodir['offset_start_cd'][1],
+ 'zipfile_comment' => $eodir['zipfile_comment'],
+ );
+
+ // Then, load file list
+ fseek($fh, $this->end_of_central['offset_start_cd']);
+ $signature = fread($fh, 4);
+
+ while ($signature == $this->dir_signature)
+ {
+ $dir['version_madeby'] = unpack("v", fread($fh, 2)); // version made by
+ $dir['version_needed'] = unpack("v", fread($fh, 2)); // version needed to extract
+ $dir['general_bit_flag'] = unpack("v", fread($fh, 2)); // general purpose bit flag
+ $dir['compression_method'] = unpack("v", fread($fh, 2)); // compression method
+ $dir['lastmod_time'] = unpack("v", fread($fh, 2)); // last mod file time
+ $dir['lastmod_date'] = unpack("v", fread($fh, 2)); // last mod file date
+ $dir['crc-32'] = fread($fh, 4); // crc-32
+ $dir['compressed_size'] = unpack("V", fread($fh, 4)); // compressed size
+ $dir['uncompressed_size'] = unpack("V", fread($fh, 4)); // uncompressed size
+ $zip_file_length = unpack("v", fread($fh, 2)); // filename length
+ $extra_field_length = unpack("v", fread($fh, 2)); // extra field length
+ $fileCommentLength = unpack("v", fread($fh, 2)); // file comment length
+ $dir['disk_number_start'] = unpack("v", fread($fh, 2)); // disk number start
+ $dir['internal_attributes'] = unpack("v", fread($fh, 2)); // internal file attributes-byte1
+ $dir['external_attributes1'] = unpack("v", fread($fh, 2)); // external file attributes-byte2
+ $dir['external_attributes2'] = unpack("v", fread($fh, 2)); // external file attributes
+ $dir['relative_offset'] = unpack("V", fread($fh, 4)); // relative offset of local header
+ $dir['file_name'] = fread($fh, $zip_file_length[1]); // filename
+ $dir['extra_field'] = $extra_field_length[1] ? fread($fh, $extra_field_length[1]) : ''; // extra field
+ $dir['file_comment'] = $fileCommentLength[1] ? fread($fh, $fileCommentLength[1]) : ''; // file comment
+
+ // Convert the date and time, from MS-DOS format to UNIX Timestamp
+ $binary_mod_date = str_pad(decbin($dir['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
+ $binary_mod_time = str_pad(decbin($dir['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
+ $last_mod_year = bindec(substr($binary_mod_date, 0, 7)) + 1980;
+ $last_mod_month = bindec(substr($binary_mod_date, 7, 4));
+ $last_mod_day = bindec(substr($binary_mod_date, 11, 5));
+ $last_mod_hour = bindec(substr($binary_mod_time, 0, 5));
+ $last_mod_minute = bindec(substr($binary_mod_time, 5, 6));
+ $last_mod_second = bindec(substr($binary_mod_time, 11, 5));
+
+ $this->central_dir_list[$dir['file_name']] = array(
+ 'version_madeby' => $dir['version_madeby'][1],
+ 'version_needed' => $dir['version_needed'][1],
+ 'general_bit_flag' => str_pad(decbin($dir['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
+ 'compression_method' => $dir['compression_method'][1],
+ 'lastmod_datetime' => mktime($last_mod_hour, $last_mod_minute, $last_mod_second, $last_mod_month, $last_mod_day, $last_mod_year),
+ 'crc-32' => str_pad(dechex(ord($dir['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
+ str_pad(dechex(ord($dir['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
+ str_pad(dechex(ord($dir['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
+ str_pad(dechex(ord($dir['crc-32'][0])), 2, '0', STR_PAD_LEFT),
+ 'compressed_size' => $dir['compressed_size'][1],
+ 'uncompressed_size' => $dir['uncompressed_size'][1],
+ 'disk_number_start' => $dir['disk_number_start'][1],
+ 'internal_attributes' => $dir['internal_attributes'][1],
+ 'external_attributes1' => $dir['external_attributes1'][1],
+ 'external_attributes2' => $dir['external_attributes2'][1],
+ 'relative_offset' => $dir['relative_offset'][1],
+ 'file_name' => $dir['file_name'],
+ 'extra_field' => $dir['extra_field'],
+ 'file_comment' => $dir['file_comment'],
+ );
+
+ $signature = fread($fh, 4);
+ }
+
+ // If loaded centralDirs, then try to identify the offsetPosition of the compressed data.
+ if ($this->central_dir_list)
+ {
+ foreach ($this->central_dir_list as $filename => $details)
+ {
+ $i = $this->_get_file_header($fh, $details['relative_offset']);
+
+ $this->compressed_list[$filename]['file_name'] = $filename;
+ $this->compressed_list[$filename]['compression_method'] = $details['compression_method'];
+ $this->compressed_list[$filename]['version_needed'] = $details['version_needed'];
+ $this->compressed_list[$filename]['lastmod_datetime'] = $details['lastmod_datetime'];
+ $this->compressed_list[$filename]['crc-32'] = $details['crc-32'];
+ $this->compressed_list[$filename]['compressed_size'] = $details['compressed_size'];
+ $this->compressed_list[$filename]['uncompressed_size'] = $details['uncompressed_size'];
+ $this->compressed_list[$filename]['lastmod_datetime'] = $details['lastmod_datetime'];
+ $this->compressed_list[$filename]['extra_field'] = $i['extra_field'];
+ $this->compressed_list[$filename]['contents_start_offset'] = $i['contents_start_offset'];
+
+ if (strtolower($stop_on_file) == strtolower($filename))
+ {
+ break;
+ }
+ }
+ }
+
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ private function _load_files_by_signatures(&$fh, $stop_on_file = FALSE)
+ {
+ fseek($fh, 0);
+
+ $return = FALSE;
+ for (;;)
+ {
+ $details = $this->_get_file_header($fh);
+
+ if ( ! $details)
+ {
+ $this->set_debug('Invalid signature. Trying to verify if is old style Data Descriptor...');
+ fseek($fh, 12 - 4, SEEK_CUR); // 12: Data descriptor - 4: Signature (that will be read again)
+ $details = $this->_get_file_header($fh);
+ }
+
+ if ( ! $details)
+ {
+ $this->set_debug('Still invalid signature. Probably reached the end of the file.');
+ break;
+ }
+
+ $filename = $details['file_name'];
+ $this->compressed_list[$filename] = $details;
+ $return = true;
+
+ if (strtolower($stop_on_file) == strtolower($filename))
+ {
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ private function _get_file_header(&$fh, $start_offset = FALSE)
+ {
+ if ($start_offset !== FALSE)
+ {
+ fseek($fh, $start_offset);
+ }
+
+ $signature = fread($fh, 4);
+
+ if ($signature == $this->zip_signature)
+ {
+ // Get information about the zipped file
+ $file['version_needed'] = unpack("v", fread($fh, 2)); // version needed to extract
+ $file['general_bit_flag'] = unpack("v", fread($fh, 2)); // general purpose bit flag
+ $file['compression_method'] = unpack("v", fread($fh, 2)); // compression method
+ $file['lastmod_time'] = unpack("v", fread($fh, 2)); // last mod file time
+ $file['lastmod_date'] = unpack("v", fread($fh, 2)); // last mod file date
+ $file['crc-32'] = fread($fh, 4); // crc-32
+ $file['compressed_size'] = unpack("V", fread($fh, 4)); // compressed size
+ $file['uncompressed_size'] = unpack("V", fread($fh, 4)); // uncompressed size
+ $zip_file_length = unpack("v", fread($fh, 2)); // filename length
+ $extra_field_length = unpack("v", fread($fh, 2)); // extra field length
+ $file['file_name'] = fread($fh, $zip_file_length[1]); // filename
+ $file['extra_field'] = $extra_field_length[1] ? fread($fh, $extra_field_length[1]) : ''; // extra field
+ $file['contents_start_offset'] = ftell($fh);
+
+ // Bypass the whole compressed contents, and look for the next file
+ fseek($fh, $file['compressed_size'][1], SEEK_CUR);
+
+ // Convert the date and time, from MS-DOS format to UNIX Timestamp
+ $binary_mod_date = str_pad(decbin($file['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
+ $binary_mod_time = str_pad(decbin($file['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
+
+ $last_mod_year = bindec(substr($binary_mod_date, 0, 7)) + 1980;
+ $last_mod_month = bindec(substr($binary_mod_date, 7, 4));
+ $last_mod_day = bindec(substr($binary_mod_date, 11, 5));
+ $last_mod_hour = bindec(substr($binary_mod_time, 0, 5));
+ $last_mod_minute = bindec(substr($binary_mod_time, 5, 6));
+ $last_mod_second = bindec(substr($binary_mod_time, 11, 5));
+
+ // Mount file table
+ $i = array(
+ 'file_name' => $file['file_name'],
+ 'compression_method' => $file['compression_method'][1],
+ 'version_needed' => $file['version_needed'][1],
+ 'lastmod_datetime' => mktime($last_mod_hour, $last_mod_minute, $last_mod_second, $last_mod_month, $last_mod_day, $last_mod_year),
+ 'crc-32' => str_pad(dechex(ord($file['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
+ str_pad(dechex(ord($file['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
+ str_pad(dechex(ord($file['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
+ str_pad(dechex(ord($file['crc-32'][0])), 2, '0', STR_PAD_LEFT),
+ 'compressed_size' => $file['compressed_size'][1],
+ 'uncompressed_size' => $file['uncompressed_size'][1],
+ 'extra_field' => $file['extra_field'],
+ 'general_bit_flag' => str_pad(decbin($file['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
+ 'contents_start_offset' => $file['contents_start_offset']
+ );
+
+ return $i;
+ }
+
+ return FALSE;
+ }
+}
+
+/* End of file Unzip.php */
+/* Location: ./system/libraries/Unzip.php */
\ No newline at end of file
diff --git a/app/modules/gallery/models/gallery_image_model.php b/app/modules/gallery/models/gallery_image_model.php
new file mode 100644
index 00000000..41859759
--- /dev/null
+++ b/app/modules/gallery/models/gallery_image_model.php
@@ -0,0 +1,179 @@
+db->where('content_id',$content_id);
+ $this->db->order_by('gallery_image_featured DESC, gallery_image_order ASC');
+ $result = $this->db->get('gallery_images');
+
+ $images = array();
+ foreach ($result->result_array() as $image) {
+ $images[] = array(
+ 'id' => $image['gallery_image_id'],
+ 'path' => setting('path_writeable') . 'gallery_images/' . $image['gallery_image_filename'],
+ 'url' => site_url(str_replace(FCPATH,'',setting('path_writeable') . 'gallery_images/' . $image['gallery_image_filename'])),
+ 'featured' => ($image['gallery_image_featured'] == '1') ? TRUE : FALSE
+ );
+ }
+
+ return $images;
+ }
+
+ /**
+ * Add Image
+ *
+ * Adds an image to an image gallery
+ *
+ * @param int $content_id Gallery content ID
+ * @param string $filename The filename of the file in /writeable/gallery_images/
+ *
+ * @return int $image_id
+ */
+ function add_image ($content_id, $filename) {
+ // get next order
+ $this->db->where('content_id',$content_id);
+ $this->db->order_by('gallery_image_order','DESC');
+ $result = $this->db->get('gallery_images');
+
+ if ($result->num_rows() > 0) {
+ $last_field = $result->row_array();
+ $order = $last_field['gallery_image_order'] + 1;
+ }
+ else {
+ $order = '1';
+ }
+
+ $insert_fields = array(
+ 'content_id' => $content_id,
+ 'gallery_image_filename' => $filename,
+ 'gallery_image_order' => $order,
+ 'gallery_image_uploaded' => date('Y-m-d H:i:s')
+ );
+
+ $this->db->insert('gallery_images',$insert_fields);
+
+ return $this->db->insert_id();
+ }
+
+ /**
+ * Remove Image
+ *
+ * Removes an image from the gallery
+ *
+ * @param int $image_id
+ *
+ * @return void
+ */
+ function remove_image ($image_id) {
+ $this->db->select('content_id');
+ $this->db->where('gallery_image_id',$image_id);
+ $result = $this->db->get('gallery_images');
+
+ $image = $result->row_array();
+ if (!isset($image['content_id'])) {
+ return FALSE;
+ }
+ $content_id = $image['content_id'];
+
+ $this->db->delete('gallery_images',array('gallery_image_id' => $image_id));
+
+ // replace feature image?
+ $this->db->select('gallery_image_id');
+ $this->db->where('content_id',$content_id);
+ $this->db->where('gallery_image_featured','1');
+ if ($this->db->get('gallery_images')->num_rows() == 0) {
+ // there's no feature image
+ $this->db->select('gallery_image_id');
+ $this->db->where('content_id',$content_id);
+ $this->db->order_by('gallery_image_order','ASC');
+ $this->db->limit(1);
+ $result = $this->db->get('gallery_images');
+ if ($result->num_rows() == 0) {
+ // no images to be a feature
+ return;
+ }
+ else {
+ $image = $result->row_array();
+ $image_id = $image['gallery_image_id'];
+
+ $this->db->update('gallery_images',array('gallery_image_featured' => '1'),array('gallery_image_id' => $image_id));
+ }
+ }
+ }
+
+ /**
+ * Make Featured Image
+ *
+ * Makes this image the featured image
+ *
+ * @param int $image_id The image ID
+ *
+ * @return boolean TRUE
+ */
+ function make_feature_image ($image_id) {
+ $this->db->where('gallery_image_id',$image_id);
+ $result = $this->db->get('gallery_images');
+
+ $image = $result->row_array();
+ $content_id = $image['content_id'];
+
+ // null all other features for this gallery
+ $this->db->update('gallery_images',array('gallery_image_featured' => '0'),array('content_id' => $content_id));
+
+ // make this the feature
+ $this->db->update('gallery_images',array('gallery_image_featured' => '1'),array('gallery_image_id' => $image_id));
+
+ $this->db->update('galleries',array('feature_image' => setting('path_writeable') . 'gallery_images/' . $image['gallery_image_filename']),array('content_id' => $content_id));
+
+ return TRUE;
+ }
+
+ /**
+ * Reset Order for Images
+ *
+ * @param int $content_id
+ *
+ */
+ function images_reset_order ($content_id) {
+ $this->db->update('gallery_images',array('gallery_image_order' => '0'), array('content_id' => $content_id));
+ }
+
+ /**
+ * Update Order
+ *
+ * @param int $field_id
+ * @param int $new_order
+ *
+ * @return void
+ */
+ function image_update_order ($image_id, $new_order) {
+ $this->db->update('gallery_images',array('gallery_image_order' => $new_order), array('gallery_image_id' => $image_id));
+ }
+}
\ No newline at end of file
diff --git a/app/modules/gallery/stylesheets/gallery.css b/app/modules/gallery/stylesheets/gallery.css
new file mode 100644
index 00000000..ffae473d
--- /dev/null
+++ b/app/modules/gallery/stylesheets/gallery.css
@@ -0,0 +1,65 @@
+div.gallery_images {
+ border: 1px solid #dedede;
+ background-color: #f0f0f0;
+ padding: 15px;
+ margin-top: 10px;
+ width: 750;
+}
+
+div.gallery_images {
+ margin: 0;
+ padding-bottom: 5px;
+}
+
+div.gallery_images ul {
+ margin: 0;
+ list-style-type: none;
+ clear: both;
+ padding: 0;
+ border: 0;
+}
+
+div.gallery_images ul li {
+ display: block;
+ float: left;
+ height: 125px;
+ width: 100px;
+ border: 1px solid #dedede;
+ background-color: #fff;
+ padding: 10px;
+ margin-right: 20px;
+ position: relative;
+ margin-bottom: 10px;
+}
+
+div.gallery_images a.delete {
+ position: absolute;
+ bottom: 10px;
+ right: 10px;
+}
+
+div.gallery_images a.move {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ cursor: move;
+}
+
+div.gallery_images a.feature {
+ position: absolute;
+ bottom: 10px;
+ right: 32px;
+}
+
+div.gallery_images span.featured {
+ display: block;
+ background-color: #57ac36;
+ padding: 1px 3px;
+ color: #fff;
+ position: absolute;
+ top: 10px;
+ width: 94px;
+ text-align: center;
+ font-size: 8pt;
+ text-transform: uppercase;
+}
diff --git a/app/modules/gallery/views/create.php b/app/modules/gallery/views/create.php
new file mode 100644
index 00000000..7cf11ec2
--- /dev/null
+++ b/app/modules/gallery/views/create.php
@@ -0,0 +1,71 @@
+=$this->load->view(branded_view('cp/header'));?>
+
+Custom collection data fields allow you to store unique data for each product. Each datum is represented by a custom field
+that is seen during the adding and editing of collections. Developers can then use this custom field data in your
+templates or as part of a larger customization.
+Would you like to enable custom collection data?
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/enable_custom_fields.php b/app/modules/store/views/enable_custom_fields.php
new file mode 100644
index 00000000..3a10a982
--- /dev/null
+++ b/app/modules/store/views/enable_custom_fields.php
@@ -0,0 +1,12 @@
+=$this->load->view(branded_view('cp/header'));?>
+Enable custom product data fields?
+
+Custom product data fields allow you to store unique data for each product. Each datum is represented by a custom field
+that is seen during the adding and editing of products. Developers can then use this custom field data in your
+templates or as part of a larger customization.
+Would you like to enable custom product data?
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/product.php b/app/modules/store/views/product.php
new file mode 100644
index 00000000..e318bc7b
--- /dev/null
+++ b/app/modules/store/views/product.php
@@ -0,0 +1,71 @@
+=$this->head_assets->javascript('js/jquery-ui-1.8.2.min.js');?>
+=$this->head_assets->javascript('js/image_gallery.js');?>
+=$this->head_assets->javascript('js/image_gallery_form.js');?>
+
+=$this->load->view(branded_view('cp/header'));?>
+
+Product: =$product['name'];?>
+
+ Product Details
+
+ - Price=setting('currency_symbol');?>=$product['price'];?>
+ if ($product['member_tiers'] != FALSE) { ?>
+ foreach ($product['member_tiers'] as $group => $price) { ?>
+ - Price (=$usergroups[$group];?>)=setting('currency_symbol');?>=$price;?>
+ } ?>
+ } ?>
+ - Weight=$product['weight'];?> =setting('weight_unit');?>
+ - Taxable if ($product['is_taxable'] == TRUE) { ?>Yes } else { ?>No } ?>
+ - Require Shipping if ($product['requires_shipping'] == TRUE) { ?>Yes } else { ?>No } ?>
+ if ($product['sku'] != '') { ?>- SKU=$product['sku'];?>
} ?>
+ if ($product['track_inventory'] == TRUE) { ?>
+ - Quantity in Stock=$product['inventory'];?>
+ - Allow Overselling? if ($product['inventory_allow_oversell'] == TRUE) { ?>Yes } else { ?>No } ?>
+ } ?>
+ if ($product['is_download'] == TRUE) { ?>
+ - Product File=$product['download_name'];?> (=format_size($product['download_size']);?>)
+ } ?>
+ if ($product['promotion'] != FALSE) { ?>
+ - Purchase Group=$usergroups[$product['promotion']];?>
+ } ?>
+ if (is_array($custom_fields)) { ?>
+ foreach ($custom_fields as $field) { ?>
+ if (@is_array(unserialize($product[$field['name']]))) { ?>
+ $product[$field['name']] = implode(', ', unserialize($product[$field['name']])); ?>
+ } ?>
+
+ - =$field['friendly_name'];?> =$product[$field['name']];?>
+ } ?>
+ } ?>
+
+
+
+=$product['description'];?>
+
+Product Images
+
+
+=$gallery;?>
+ if (!empty($product['images'])) { ?>
+
+ } ?>
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/product_edit.php b/app/modules/store/views/product_edit.php
new file mode 100644
index 00000000..7d6f5825
--- /dev/null
+++ b/app/modules/store/views/product_edit.php
@@ -0,0 +1,164 @@
+=$this->head_assets->javascript('js/product.js');?>
+=$this->head_assets->javascript('js/image_gallery_form.js');?>
+
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+
+=$form;?>
+
+
+
+
+
+
+
+
+
+ if (!empty($custom_fields)) { ?>
+
+ } ?>
+
+
+
+
+
+
+
+
+ if (!empty($shared_product_options)) { ?>
+ Existing Product Options
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+ } ?>
+
+ New Product Option
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+ -
+ Save for use with other products
+
+
+
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/product_form.php b/app/modules/store/views/product_form.php
new file mode 100644
index 00000000..070469a1
--- /dev/null
+++ b/app/modules/store/views/product_form.php
@@ -0,0 +1,162 @@
+ FALSE
+ );
+}
+
+?>
+
+=$this->head_assets->javascript('js/product.js');?>
+=$this->head_assets->javascript('js/image_gallery_form.js');?>
+
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+
+=$form;?>
+
+
+
+
+
+
+
+
+
+=$gallery;?>
+
+ if (!empty($custom_fields)) { ?>
+
+ } ?>
+
+
+
+If you are not receiving a server response after submitting this form, you are likely surpassing the total file upload
+ limits of your server. To bypass this: (1) upload your product downloads via FTP and, (2) add your images after submitting the product, in the "image gallery editor".
+ You can also contact your server administrator to increase these upload limits.
+
+
+
+ if (!empty($shared_product_options)) { ?>
+ Existing Product Options
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+ } ?>
+
+ New Product Option
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+ -
+ Save for use with other products
+
+
+
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/product_option_edit.php b/app/modules/store/views/product_option_edit.php
new file mode 100644
index 00000000..cd690bfc
--- /dev/null
+++ b/app/modules/store/views/product_option_edit.php
@@ -0,0 +1,46 @@
+=$this->head_assets->javascript('js/product_option.js');?>
+=$this->head_assets->javascript('js/form.js');?>
+
+=$this->load->view(branded_view('cp/header'));?>
+
+Product Option: =$option_name;?>
+
+
+
+
+
+=$form?>
+
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/product_option_form.php b/app/modules/store/views/product_option_form.php
new file mode 100644
index 00000000..2812f4a5
--- /dev/null
+++ b/app/modules/store/views/product_option_form.php
@@ -0,0 +1,42 @@
+=$this->head_assets->javascript('js/product_option.js');?>
+=$this->head_assets->javascript('js/form.js');?>
+
+=$this->load->view(branded_view('cp/header'));?>
+
+Create New Product Option
+
+
+
+=$form?>
+
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/product_options.php b/app/modules/store/views/product_options.php
new file mode 100644
index 00000000..e0e004a8
--- /dev/null
+++ b/app/modules/store/views/product_options.php
@@ -0,0 +1,34 @@
+load->view(branded_view('cp/header')); ?>
+Manage Product Options
+
+dataset->table_head(); ?>
+
+dataset->data)) : ?>
+ dataset->data as $row) :?>
+
+
+ =$row['id'];?>
+ =$row['name'];?>
+
+
+
+ edit
+
+
+
+
+ No product options match your filters.
+
+
+
+dataset->table_close(); ?>
+
+load->view(branded_view('cp/footer')); ?>
\ No newline at end of file
diff --git a/app/modules/store/views/products.php b/app/modules/store/views/products.php
new file mode 100644
index 00000000..2c57b9b2
--- /dev/null
+++ b/app/modules/store/views/products.php
@@ -0,0 +1,56 @@
+=$this->load->view(branded_view('cp/header'));?>
+Manage Products
+=$this->dataset->table_head();?>
+
+if (!empty($this->dataset->data)) {
+ foreach ($this->dataset->data as $row) {
+ ?>
+
+
+ =$row['id'];?>
+ =admin_link($row['url']);?>
+ if ($row['is_download'] == TRUE) { ?>
} ?> if ($row['is_download'] == TRUE and $row['requires_shipping'] == TRUE) { ?> } ?> if ($row['requires_shipping'] == TRUE) { ?>
} ?>
+ =$row['name'];?>
+ =setting('currency_symbol');?>=$row['price'];?>
+ =$row['inventory'];?>
+
+
+
+
+
+
+
+
+
+
+
+ }
+}
+else {
+?>
+
+ No products match your filters.
+
+ } ?>
+=$this->dataset->table_close();?>
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/shipping_config.php b/app/modules/store/views/shipping_config.php
new file mode 100644
index 00000000..80330fab
--- /dev/null
+++ b/app/modules/store/views/shipping_config.php
@@ -0,0 +1,11 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+
+=$form;?>
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/shipping_form.php b/app/modules/store/views/shipping_form.php
new file mode 100644
index 00000000..fdbfdf38
--- /dev/null
+++ b/app/modules/store/views/shipping_form.php
@@ -0,0 +1,11 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+
+=$form;?>
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/shipping_rates.php b/app/modules/store/views/shipping_rates.php
new file mode 100644
index 00000000..fd937517
--- /dev/null
+++ b/app/modules/store/views/shipping_rates.php
@@ -0,0 +1,31 @@
+=$this->load->view(branded_view('cp/header'));?>
+Manage Shipping Rates
+=$this->dataset->table_head();?>
+
+if (!empty($this->dataset->data)) {
+ foreach ($this->dataset->data as $row) {
+ ?>
+
+
+ =$row['id'];?>
+ =$row['name'];?>
+ =$row['state'];?>
+ =$row['country'];?>
+
+ if ($row['type'] == 'weight') { ?>=setting('currency_symbol');?>=$row['rate'];?> per =setting('weight_unit');?> } ?>
+ if ($row['type'] == 'product') { ?>=setting('currency_symbol');?>=$row['rate'];?> per product } ?>
+ if ($row['type'] == 'flat') { ?>=setting('currency_symbol');?>=$row['rate'];?> flat fee } ?>
+
+ edit
+
+
+ }
+}
+else {
+?>
+
+ No shipping rates match your filters. Add a new shipping rate.
+
+ } ?>
+=$this->dataset->table_close();?>
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/tax_form.php b/app/modules/store/views/tax_form.php
new file mode 100644
index 00000000..c2f9ef6b
--- /dev/null
+++ b/app/modules/store/views/tax_form.php
@@ -0,0 +1,11 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+
+=$form;?>
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/store/views/tax_rules.php b/app/modules/store/views/tax_rules.php
new file mode 100644
index 00000000..e23df1d5
--- /dev/null
+++ b/app/modules/store/views/tax_rules.php
@@ -0,0 +1,27 @@
+=$this->load->view(branded_view('cp/header'));?>
+Manage Tax Rules
+=$this->dataset->table_head();?>
+
+if (!empty($this->dataset->data)) {
+ foreach ($this->dataset->data as $row) {
+ ?>
+
+
+ =$row['id'];?>
+ =$row['state'];?>
+ =$row['country'];?>
+ =$row['percentage'];?>%
+ =$row['name'];?>
+ edit
+
+
+ }
+}
+else {
+?>
+
+ No tax rules match your filters. Add a new tax rule.
+
+ } ?>
+=$this->dataset->table_close();?>
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/twitter/controllers/admincp.php b/app/modules/twitter/controllers/admincp.php
new file mode 100644
index 00000000..0eac31b5
--- /dev/null
+++ b/app/modules/twitter/controllers/admincp.php
@@ -0,0 +1,285 @@
+admin_navigation->parent_active('configuration');
+ }
+
+ function index() {
+ // we'll show varying levels of blank state screens
+
+ // do we have a consumer key and secret?
+ if (setting('twitter_consumer_key') == '' or setting('twitter_consumer_secret') == '') {
+ return redirect('admincp/twitter/register_application');
+ }
+
+ // we have an app setup, but we may need the OAuth token...
+
+ if (setting('twitter_oauth_token') == '' or setting('twitter_oauth_token_secret') == '') {
+ return redirect('admincp/twitter/update_oauth');
+ }
+
+ // we have oauth tokens, but let's make sure they are up-to-date
+ require(APPPATH . 'modules/twitter/libraries/twitteroauth.php');
+ $connection = new TwitterOAuth(setting('twitter_consumer_key'), setting('twitter_consumer_secret'), setting('twitter_oauth_token'), setting('twitter_oauth_token_secret'));
+
+ $test = $connection->get('account/verify_credentials');
+
+ if ($connection->http_code != 200) {
+ // connection failed
+ $this->notices->SetError('Your Twitter OAuth tokens are out of date! Please re-authorize.');
+
+ return redirect('admincp/twitter/update_oauth');
+ }
+
+ // Twitter credentials are solid!
+
+ $this->admin_navigation->module_link('Update Application Credentials',site_url('admincp/twitter/register_application'));
+ $this->admin_navigation->module_link('Update Authorization Tokens',site_url('admincp/twitter/update_oauth'));
+
+ $this->load->library('admin_form');
+ $form = new Admin_form;
+ $form->fieldset('Twitter Configuration');
+
+ // get content types
+ $this->load->model('publish/content_type_model');
+ $types = $this->content_type_model->get_content_types(array('is_standard' => '1'));
+ $type_options = array();
+ foreach ($types as $type) {
+ $type_options[$type['id']] = $type['name'];
+ }
+
+ // get topics
+ $this->load->model('publish/topic_model');
+ $topics = $this->topic_model->get_tiered_topics();
+ $topic_options = array();
+ $topic_options[0] = 'Any Topic';
+ foreach ($topics as $topic) {
+ $topic_options[$topic['id']] = $topic['name'];
+ }
+
+ $form->value_row(' ','Configure ' . setting('app_name') . ' to automatically tweet your latest content with the options below.');
+
+ $checked = (setting('twitter_enabled') == '1') ? TRUE : FALSE;
+ $form->checkbox('Enable Tweeting?','enabled', '1', $checked);
+
+ $selected = (setting('twitter_content_types') != '') ? unserialize(setting('twitter_content_types')) : array();
+ $form->dropdown('Content Types','content_types', $type_options, $selected, TRUE, FALSE, 'Only posts of these content types will be tweeted.');
+
+ $selected = (setting('twitter_topics') != '') ? unserialize(setting('twitter_topics')) : array();
+ $form->dropdown('Topics','topics', $topic_options, $selected, TRUE, FALSE, 'Only posts in these topics will be tweeted.');
+
+ //$value = (setting('twitter_template') == '') ? '[TITLE]: [URL]' : setting('twitter_template');
+ //$form->textarea('Tweet Template','template',$value,'Specify the format of the Tweet. You may use the tags: [SITE_NAME], [TITLE], and [URL].');
+
+ $data = array(
+ 'form_title' => 'Twitter: Configuration',
+ 'form_action' => site_url('admincp/twitter/post_config'),
+ 'form' => $form->display(),
+ 'form_button' => 'Save Configuration'
+ );
+
+ $this->load->view('generic', $data);
+ }
+
+ function post_config () {
+ $this->settings_model->update_setting('twitter_content_types',serialize($_POST['content_types']));
+ $topics = $this->input->post('topics');
+ if(empty($topics)){
+ $topics = 0;
+ }
+ $this->settings_model->update_setting('twitter_topics',serialize($topics));
+ $this->settings_model->update_setting('twitter_enabled', $this->input->post('enabled'));
+ //$this->settings_model->update_setting('twitter_template', $this->input->post('template'));
+
+ $this->notices->SetNotice('Twitter configuration saved.');
+
+ redirect('admincp/twitter');
+ }
+
+ function oauth_callback () {
+ require(APPPATH . 'modules/twitter/libraries/twitteroauth.php');
+
+ /* If the oauth_token is old redirect to the connect page. */
+ if ($this->input->get('oauth_token') && $this->session->userdata('twitter_oauth_token') !== $this->input->get('oauth_token')) {
+ return redirect('admincp/twitter');
+ }
+
+ /* Create TwitteroAuth object with app key/secret and token key/secret from default phase */
+ $connection = new TwitterOAuth(setting('twitter_consumer_key'), setting('twitter_consumer_secret'), $this->session->userdata('twitter_oauth_token'), $this->session->userdata('twitter_oauth_token_secret'));
+
+ /* Request access tokens from twitter */
+ $access_token = $connection->getAccessToken($this->input->get('oauth_verifier'));
+
+ /* Save the access tokens. Normally these would be saved in a database for future use. */
+ $this->settings_model->update_setting('twitter_oauth_token', $access_token['oauth_token']);
+ $this->settings_model->update_setting('twitter_oauth_token_secret', $access_token['oauth_token_secret']);
+
+ /* Remove no longer needed request tokens */
+ $this->session->unset_userdata('twitter_oauth_token');
+ $this->session->unset_userdata('twitter_oauth_token_secret');
+
+ /* If HTTP response is 200 continue otherwise send to connect page to retry */
+ if (200 == $connection->http_code) {
+ $this->notices->SetNotice('OAuthorization retrieved successfully.');
+ return redirect('admincp/twitter');
+ } else {
+ $this->notices->SetError('Error making connection in OAuth callback.');
+ return redirect('admincp/twitter');
+ }
+ }
+
+ function post_update_oauth () {
+ require(APPPATH . 'modules/twitter/libraries/twitteroauth.php');
+
+ $connection = new TwitterOAuth(setting('twitter_consumer_key'), setting('twitter_consumer_secret'));
+
+ $request_token = $connection->getRequestToken(site_url('admincp/twitter/oauth_callback'));
+
+ /* Save temporary credentials to session. */
+ $this->session->set_userdata('twitter_oauth_token', $request_token['oauth_token']);
+ $this->session->set_userdata('twitter_oauth_token_secret', $request_token['oauth_token_secret']);
+
+ /* If last connection failed don't display authorization link. */
+ switch ($connection->http_code) {
+ case 200:
+ /* Build authorize URL and redirect user to Twitter. */
+ $url = $connection->getAuthorizeURL($request_token['oauth_token']);
+ header('Location: ' . $url);
+ break;
+ default:
+ die(show_error('Could not connect to Twitter. Refresh the page or try again later.'));
+ }
+ }
+
+ function update_oauth () {
+ $this->load->library('admin_form');
+ $form = new Admin_form;
+
+ $form->fieldset('Introduction');
+ $form->value_row(' ','Your OAuth credentials are either non-existent or out-of-date. These are
+ required for ' . setting('app_name') . ' to have permission to post on your behalf in your Twitter account.');
+
+ $form->value_row(' ','Click the button below to authorize ' . setting('app_name') . ' to post on your behalf. No Tweets will be
+ sent out until you select which content to post and how often to do so.');
+
+ $data = array(
+ 'form_title' => 'Twitter: Update Your Authorization',
+ 'form_action' => site_url('admincp/twitter/post_update_oauth'),
+ 'form' => $form->display(),
+ 'form_button' => 'Authorize ' . setting('app_name') . ' Now!'
+ );
+
+ $this->load->view('generic', $data);
+ }
+
+ function post_register_application () {
+ $this->load->library('form_validation');
+ $this->form_validation->set_rules('consumer_key','Consumer Key','required');
+ $this->form_validation->set_rules('consumer_secret','Consumer Secret','required');
+
+ if ($this->form_validation->run() === FALSE) {
+ $this->notices->SetError(validation_errors());
+
+ return redirect('admincp/register_application');
+ }
+ else {
+ $this->settings_model->update_setting('twitter_consumer_key', $this->input->post('consumer_key'));
+ $this->settings_model->update_setting('twitter_consumer_secret', $this->input->post('consumer_secret'));
+
+ $this->notices->SetNotice('Twitter application registered successfully.');
+
+ return redirect('admincp/twitter');
+ }
+ }
+
+ function register_application () {
+ $this->load->library('admin_form');
+ $form = new Admin_form;
+
+ $form->fieldset('Introduction');
+ $form->value_row(' ','Before you connect your ' . setting('app_name') . ' installation to Twitter, you must register an
+ application at Twitter. This sounds more complex than it really is: All you have to do is go and
+ complete a form telling them about your website and you\'ll instantly receive a Consumer Key and a
+ Consumer Secret that you will then enter below.
+ Be sure to specify that your app is a BROWSER app and enter a Callback URL of "' . site_url() . '".');
+
+ $form->value_row(' ','Click here to register your application at Twitter.');
+
+ $form->text('Consumer Key','consumer_key');
+ $form->text('Consumer Secret','consumer_secret');
+
+ $data = array(
+ 'form_title' => 'Twitter: Register Your Application',
+ 'form_action' => site_url('admincp/twitter/post_register_application'),
+ 'form' => $form->display(),
+ 'form_button' => 'Save Application Credentials'
+ );
+
+ $this->load->view('generic', $data);
+ }
+
+ /*
+ * TODO
+ * Tweet Logs displays all of the tweets sent out
+ */
+ function tweet_logs () {
+ $this->load->library('dataset');
+
+ $this->load->model('twitter_model');
+ $tweets = $this->twitter_model->get_tweets();
+
+ $columns = array(
+ array(
+ 'name' => 'ID #',
+ 'type' => 'id',
+ 'width' => '5%'
+ )
+ ,array(
+ 'name' => 'Date',
+ 'type' => 'date',
+ 'sort_column' => 'sent_time',
+ 'width' => '20%',
+ 'filter' => 'timestamp',
+ 'field_start_date' => 'start_date',
+ 'field_end_date' => 'end_date'
+ ),
+ array(
+ 'name' => 'tweet',
+ 'type' => 'text',
+ 'filter' => 'tweet',
+ 'sort_column' => 'tweet',
+ 'width' => '35%'
+ )
+ );
+
+ $this->dataset->columns($columns);
+ $this->dataset->datasource('twitter_model','get_tweets');
+ $this->dataset->base_url(site_url('admincp/twitter/tweet_logs'));
+
+ // total rows
+ $total_rows = $this->db->get('tweets_sent')->num_rows();
+ $this->dataset->total_rows($total_rows);
+
+ // initialize the dataset
+ $this->dataset->initialize();
+
+ $data = array('tweets' => $tweets);
+
+ $this->load->view('tweets.php', $data);
+ }
+}
\ No newline at end of file
diff --git a/app/modules/twitter/controllers/oauth_callback.php b/app/modules/twitter/controllers/oauth_callback.php
new file mode 100644
index 00000000..68e40fc7
--- /dev/null
+++ b/app/modules/twitter/controllers/oauth_callback.php
@@ -0,0 +1,50 @@
+input->get('oauth_token') && $this->session->userdata('twitter_oauth_token') !== $this->input->get('oauth_token')) {
+ return redirect('admincp/twitter');
+ }
+
+ /* Create TwitteroAuth object with app key/secret and token key/secret from default phase */
+ $connection = new TwitterOAuth(setting('twitter_consumer_key'), setting('twitter_consumer_secret'), $this->session->userdata('twitter_oauth_token'), $this->session->userdata('twitter_oauth_token_secret'));
+
+ /* Request access tokens from twitter */
+ $access_token = $connection->getAccessToken($this->input->get('oauth_verifier'));
+
+ /* Save the access tokens. Normally these would be saved in a database for future use. */
+ $this->settings_model->update_setting('twitter_oauth_token', $access_token['oauth_token']);
+ $this->settings_model->update_setting('twitter_oauth_token_secret', $access_token['oauth_token_secret']);
+
+ /* Remove no longer needed request tokens */
+ $this->session->unset_userdata('twitter_oauth_token');
+ $this->session->unset_userdata('twitter_oauth_token_secret');
+
+ /* If HTTP response is 200 continue otherwise send to connect page to retry */
+ if (200 == $connection->http_code) {
+ $this->notices->SetNotice('OAuthorization retrieved successfully.');
+ return redirect('admincp/twitter');
+ } else {
+ $this->notices->SetError('Error making connection in OAuth callback.');
+ return redirect('admincp/twitter');
+ }
+ }
+}
diff --git a/app/modules/twitter/controllers/twitter.php b/app/modules/twitter/controllers/twitter.php
new file mode 100644
index 00000000..1437ab26
--- /dev/null
+++ b/app/modules/twitter/controllers/twitter.php
@@ -0,0 +1,36 @@
+load->model('link_model');
+ $link = $this->link_model->get_links(array('url_path' => $url_path));
+
+ if (empty($link)) {
+ return show_404($url_path);
+ }
+
+ $link = $link[0];
+
+ $link = $this->db->select('link_url_path')->from('links')->where('link_id',$link['parameter'])->get()->row_array();
+
+ if (empty($link)) {
+ return show_404($url_path);
+ }
+
+ // return the template via the link's stored parameter (the mapped template file)
+ return redirect($link['link_url_path']);
+ }
+}
\ No newline at end of file
diff --git a/app/modules/twitter/libraries/oauth.php b/app/modules/twitter/libraries/oauth.php
new file mode 100644
index 00000000..27d9613c
--- /dev/null
+++ b/app/modules/twitter/libraries/oauth.php
@@ -0,0 +1,873 @@
+key = $key;
+ $this->secret = $secret;
+ $this->callback_url = $callback_url;
+ }
+
+ function __toString() {
+ return "OAuthConsumer[key=$this->key,secret=$this->secret]";
+ }
+}
+
+class OAuthToken {
+ // access tokens and request tokens
+ public $key;
+ public $secret;
+
+ /**
+ * key = the token
+ * secret = the token secret
+ */
+ function __construct($key, $secret) {
+ $this->key = $key;
+ $this->secret = $secret;
+ }
+
+ /**
+ * generates the basic string serialization of a token that a server
+ * would respond to request_token and access_token calls with
+ */
+ function to_string() {
+ return "oauth_token=" .
+ OAuthUtil::urlencode_rfc3986($this->key) .
+ "&oauth_token_secret=" .
+ OAuthUtil::urlencode_rfc3986($this->secret);
+ }
+
+ function __toString() {
+ return $this->to_string();
+ }
+}
+
+/**
+ * A class for implementing a Signature Method
+ * See section 9 ("Signing Requests") in the spec
+ */
+abstract class OAuthSignatureMethod {
+ /**
+ * Needs to return the name of the Signature Method (ie HMAC-SHA1)
+ * @return string
+ */
+ abstract public function get_name();
+
+ /**
+ * Build up the signature
+ * NOTE: The output of this function MUST NOT be urlencoded.
+ * the encoding is handled in OAuthRequest when the final
+ * request is serialized
+ * @param OAuthRequest $request
+ * @param OAuthConsumer $consumer
+ * @param OAuthToken $token
+ * @return string
+ */
+ abstract public function build_signature($request, $consumer, $token);
+
+ /**
+ * Verifies that a given signature is correct
+ * @param OAuthRequest $request
+ * @param OAuthConsumer $consumer
+ * @param OAuthToken $token
+ * @param string $signature
+ * @return bool
+ */
+ public function check_signature($request, $consumer, $token, $signature) {
+ $built = $this->build_signature($request, $consumer, $token);
+ return $built == $signature;
+ }
+}
+
+/**
+ * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104]
+ * where the Signature Base String is the text and the key is the concatenated values (each first
+ * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&'
+ * character (ASCII code 38) even if empty.
+ * - Chapter 9.2 ("HMAC-SHA1")
+ */
+class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
+ function get_name() {
+ return "HMAC-SHA1";
+ }
+
+ public function build_signature($request, $consumer, $token) {
+ $base_string = $request->get_signature_base_string();
+ $request->base_string = $base_string;
+
+ $key_parts = array(
+ $consumer->secret,
+ ($token) ? $token->secret : ""
+ );
+
+ $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
+ $key = implode('&', $key_parts);
+
+ return base64_encode(hash_hmac('sha1', $base_string, $key, true));
+ }
+}
+
+/**
+ * The PLAINTEXT method does not provide any security protection and SHOULD only be used
+ * over a secure channel such as HTTPS. It does not use the Signature Base String.
+ * - Chapter 9.4 ("PLAINTEXT")
+ */
+class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
+ public function get_name() {
+ return "PLAINTEXT";
+ }
+
+ /**
+ * oauth_signature is set to the concatenated encoded values of the Consumer Secret and
+ * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is
+ * empty. The result MUST be encoded again.
+ * - Chapter 9.4.1 ("Generating Signatures")
+ *
+ * Please note that the second encoding MUST NOT happen in the SignatureMethod, as
+ * OAuthRequest handles this!
+ */
+ public function build_signature($request, $consumer, $token) {
+ $key_parts = array(
+ $consumer->secret,
+ ($token) ? $token->secret : ""
+ );
+
+ $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
+ $key = implode('&', $key_parts);
+ $request->base_string = $key;
+
+ return $key;
+ }
+}
+
+/**
+ * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in
+ * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for
+ * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a
+ * verified way to the Service Provider, in a manner which is beyond the scope of this
+ * specification.
+ * - Chapter 9.3 ("RSA-SHA1")
+ */
+abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
+ public function get_name() {
+ return "RSA-SHA1";
+ }
+
+ // Up to the SP to implement this lookup of keys. Possible ideas are:
+ // (1) do a lookup in a table of trusted certs keyed off of consumer
+ // (2) fetch via http using a url provided by the requester
+ // (3) some sort of specific discovery code based on request
+ //
+ // Either way should return a string representation of the certificate
+ protected abstract function fetch_public_cert(&$request);
+
+ // Up to the SP to implement this lookup of keys. Possible ideas are:
+ // (1) do a lookup in a table of trusted certs keyed off of consumer
+ //
+ // Either way should return a string representation of the certificate
+ protected abstract function fetch_private_cert(&$request);
+
+ public function build_signature($request, $consumer, $token) {
+ $base_string = $request->get_signature_base_string();
+ $request->base_string = $base_string;
+
+ // Fetch the private key cert based on the request
+ $cert = $this->fetch_private_cert($request);
+
+ // Pull the private key ID from the certificate
+ $privatekeyid = openssl_get_privatekey($cert);
+
+ // Sign using the key
+ $ok = openssl_sign($base_string, $signature, $privatekeyid);
+
+ // Release the key resource
+ openssl_free_key($privatekeyid);
+
+ return base64_encode($signature);
+ }
+
+ public function check_signature($request, $consumer, $token, $signature) {
+ $decoded_sig = base64_decode($signature);
+
+ $base_string = $request->get_signature_base_string();
+
+ // Fetch the public key cert based on the request
+ $cert = $this->fetch_public_cert($request);
+
+ // Pull the public key ID from the certificate
+ $publickeyid = openssl_get_publickey($cert);
+
+ // Check the computed signature against the one passed in the query
+ $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
+
+ // Release the key resource
+ openssl_free_key($publickeyid);
+
+ return $ok == 1;
+ }
+}
+
+class OAuthRequest {
+ private $parameters;
+ private $http_method;
+ private $http_url;
+ // for debug purposes
+ public $base_string;
+ public static $version = '1.0';
+ public static $POST_INPUT = 'php://input';
+
+ function __construct($http_method, $http_url, $parameters=NULL) {
+ @$parameters or $parameters = array();
+ $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters);
+ $this->parameters = $parameters;
+ $this->http_method = $http_method;
+ $this->http_url = $http_url;
+ }
+
+
+ /**
+ * attempt to build up a request from what was passed to the server
+ */
+ public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
+ $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
+ ? 'http'
+ : 'https';
+ @$http_url or $http_url = $scheme .
+ '://' . $_SERVER['HTTP_HOST'] .
+ ':' .
+ $_SERVER['SERVER_PORT'] .
+ $_SERVER['REQUEST_URI'];
+ @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
+
+ // We weren't handed any parameters, so let's find the ones relevant to
+ // this request.
+ // If you run XML-RPC or similar you should use this to provide your own
+ // parsed parameter-list
+ if (!$parameters) {
+ // Find request headers
+ $request_headers = OAuthUtil::get_headers();
+
+ // Parse the query-string to find GET parameters
+ $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
+
+ // It's a POST request of the proper content-type, so parse POST
+ // parameters and add those overriding any duplicates from GET
+ if ($http_method == "POST"
+ && @strstr($request_headers["Content-Type"],
+ "application/x-www-form-urlencoded")
+ ) {
+ $post_data = OAuthUtil::parse_parameters(
+ file_get_contents(self::$POST_INPUT)
+ );
+ $parameters = array_merge($parameters, $post_data);
+ }
+
+ // We have a Authorization-header with OAuth data. Parse the header
+ // and add those overriding any duplicates from GET or POST
+ if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
+ $header_parameters = OAuthUtil::split_header(
+ $request_headers['Authorization']
+ );
+ $parameters = array_merge($parameters, $header_parameters);
+ }
+
+ }
+
+ return new OAuthRequest($http_method, $http_url, $parameters);
+ }
+
+ /**
+ * pretty much a helper function to set up the request
+ */
+ public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
+ @$parameters or $parameters = array();
+ $defaults = array("oauth_version" => OAuthRequest::$version,
+ "oauth_nonce" => OAuthRequest::generate_nonce(),
+ "oauth_timestamp" => OAuthRequest::generate_timestamp(),
+ "oauth_consumer_key" => $consumer->key);
+ if ($token)
+ $defaults['oauth_token'] = $token->key;
+
+ $parameters = array_merge($defaults, $parameters);
+
+ return new OAuthRequest($http_method, $http_url, $parameters);
+ }
+
+ public function set_parameter($name, $value, $allow_duplicates = true) {
+ if ($allow_duplicates && isset($this->parameters[$name])) {
+ // We have already added parameter(s) with this name, so add to the list
+ if (is_scalar($this->parameters[$name])) {
+ // This is the first duplicate, so transform scalar (string)
+ // into an array so we can add the duplicates
+ $this->parameters[$name] = array($this->parameters[$name]);
+ }
+
+ $this->parameters[$name][] = $value;
+ } else {
+ $this->parameters[$name] = $value;
+ }
+ }
+
+ public function get_parameter($name) {
+ return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
+ }
+
+ public function get_parameters() {
+ return $this->parameters;
+ }
+
+ public function unset_parameter($name) {
+ unset($this->parameters[$name]);
+ }
+
+ /**
+ * The request parameters, sorted and concatenated into a normalized string.
+ * @return string
+ */
+ public function get_signable_parameters() {
+ // Grab all parameters
+ $params = $this->parameters;
+
+ // Remove oauth_signature if present
+ // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
+ if (isset($params['oauth_signature'])) {
+ unset($params['oauth_signature']);
+ }
+
+ return OAuthUtil::build_http_query($params);
+ }
+
+ /**
+ * Returns the base string of this request
+ *
+ * The base string defined as the method, the url
+ * and the parameters (normalized), each urlencoded
+ * and the concated with &.
+ */
+ public function get_signature_base_string() {
+ $parts = array(
+ $this->get_normalized_http_method(),
+ $this->get_normalized_http_url(),
+ $this->get_signable_parameters()
+ );
+
+ $parts = OAuthUtil::urlencode_rfc3986($parts);
+
+ return implode('&', $parts);
+ }
+
+ /**
+ * just uppercases the http method
+ */
+ public function get_normalized_http_method() {
+ return strtoupper($this->http_method);
+ }
+
+ /**
+ * parses the url and rebuilds it to be
+ * scheme://host/path
+ */
+ public function get_normalized_http_url() {
+ $parts = parse_url($this->http_url);
+
+ $port = @$parts['port'];
+ $scheme = $parts['scheme'];
+ $host = $parts['host'];
+ $path = @$parts['path'];
+
+ $port or $port = ($scheme == 'https') ? '443' : '80';
+
+ if (($scheme == 'https' && $port != '443')
+ || ($scheme == 'http' && $port != '80')) {
+ $host = "$host:$port";
+ }
+ return "$scheme://$host$path";
+ }
+
+ /**
+ * builds a url usable for a GET request
+ */
+ public function to_url() {
+ $post_data = $this->to_postdata();
+ $out = $this->get_normalized_http_url();
+ if ($post_data) {
+ $out .= '?'.$post_data;
+ }
+ return $out;
+ }
+
+ /**
+ * builds the data one would send in a POST request
+ */
+ public function to_postdata() {
+ return OAuthUtil::build_http_query($this->parameters);
+ }
+
+ /**
+ * builds the Authorization: header
+ */
+ public function to_header($realm=null) {
+ $first = true;
+ if($realm) {
+ $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"';
+ $first = false;
+ } else
+ $out = 'Authorization: OAuth';
+
+ $total = array();
+ foreach ($this->parameters as $k => $v) {
+ if (substr($k, 0, 5) != "oauth") continue;
+ if (is_array($v)) {
+ throw new OAuthException('Arrays not supported in headers');
+ }
+ $out .= ($first) ? ' ' : ',';
+ $out .= OAuthUtil::urlencode_rfc3986($k) .
+ '="' .
+ OAuthUtil::urlencode_rfc3986($v) .
+ '"';
+ $first = false;
+ }
+ return $out;
+ }
+
+ public function __toString() {
+ return $this->to_url();
+ }
+
+
+ public function sign_request($signature_method, $consumer, $token) {
+ $this->set_parameter(
+ "oauth_signature_method",
+ $signature_method->get_name(),
+ false
+ );
+ $signature = $this->build_signature($signature_method, $consumer, $token);
+ $this->set_parameter("oauth_signature", $signature, false);
+ }
+
+ public function build_signature($signature_method, $consumer, $token) {
+ $signature = $signature_method->build_signature($this, $consumer, $token);
+ return $signature;
+ }
+
+ /**
+ * util function: current timestamp
+ */
+ private static function generate_timestamp() {
+ return time();
+ }
+
+ /**
+ * util function: current nonce
+ */
+ private static function generate_nonce() {
+ $mt = microtime();
+ $rand = mt_rand();
+
+ return md5($mt . $rand); // md5s look nicer than numbers
+ }
+}
+
+class OAuthServer {
+ protected $timestamp_threshold = 300; // in seconds, five minutes
+ protected $version = '1.0'; // hi blaine
+ protected $signature_methods = array();
+
+ protected $data_store;
+
+ function __construct($data_store) {
+ $this->data_store = $data_store;
+ }
+
+ public function add_signature_method($signature_method) {
+ $this->signature_methods[$signature_method->get_name()] =
+ $signature_method;
+ }
+
+ // high level functions
+
+ /**
+ * process a request_token request
+ * returns the request token on success
+ */
+ public function fetch_request_token(&$request) {
+ $this->get_version($request);
+
+ $consumer = $this->get_consumer($request);
+
+ // no token required for the initial token request
+ $token = NULL;
+
+ $this->check_signature($request, $consumer, $token);
+
+ // Rev A change
+ $callback = $request->get_parameter('oauth_callback');
+ $new_token = $this->data_store->new_request_token($consumer, $callback);
+
+ return $new_token;
+ }
+
+ /**
+ * process an access_token request
+ * returns the access token on success
+ */
+ public function fetch_access_token(&$request) {
+ $this->get_version($request);
+
+ $consumer = $this->get_consumer($request);
+
+ // requires authorized request token
+ $token = $this->get_token($request, $consumer, "request");
+
+ $this->check_signature($request, $consumer, $token);
+
+ // Rev A change
+ $verifier = $request->get_parameter('oauth_verifier');
+ $new_token = $this->data_store->new_access_token($token, $consumer, $verifier);
+
+ return $new_token;
+ }
+
+ /**
+ * verify an api call, checks all the parameters
+ */
+ public function verify_request(&$request) {
+ $this->get_version($request);
+ $consumer = $this->get_consumer($request);
+ $token = $this->get_token($request, $consumer, "access");
+ $this->check_signature($request, $consumer, $token);
+ return array($consumer, $token);
+ }
+
+ // Internals from here
+ /**
+ * version 1
+ */
+ private function get_version(&$request) {
+ $version = $request->get_parameter("oauth_version");
+ if (!$version) {
+ // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present.
+ // Chapter 7.0 ("Accessing Protected Ressources")
+ $version = '1.0';
+ }
+ if ($version !== $this->version) {
+ throw new OAuthException("OAuth version '$version' not supported");
+ }
+ return $version;
+ }
+
+ /**
+ * figure out the signature with some defaults
+ */
+ private function get_signature_method(&$request) {
+ $signature_method =
+ @$request->get_parameter("oauth_signature_method");
+
+ if (!$signature_method) {
+ // According to chapter 7 ("Accessing Protected Ressources") the signature-method
+ // parameter is required, and we can't just fallback to PLAINTEXT
+ throw new OAuthException('No signature method parameter. This parameter is required');
+ }
+
+ if (!in_array($signature_method,
+ array_keys($this->signature_methods))) {
+ throw new OAuthException(
+ "Signature method '$signature_method' not supported " .
+ "try one of the following: " .
+ implode(", ", array_keys($this->signature_methods))
+ );
+ }
+ return $this->signature_methods[$signature_method];
+ }
+
+ /**
+ * try to find the consumer for the provided request's consumer key
+ */
+ private function get_consumer(&$request) {
+ $consumer_key = @$request->get_parameter("oauth_consumer_key");
+ if (!$consumer_key) {
+ throw new OAuthException("Invalid consumer key");
+ }
+
+ $consumer = $this->data_store->lookup_consumer($consumer_key);
+ if (!$consumer) {
+ throw new OAuthException("Invalid consumer");
+ }
+
+ return $consumer;
+ }
+
+ /**
+ * try to find the token for the provided request's token key
+ */
+ private function get_token(&$request, $consumer, $token_type="access") {
+ $token_field = @$request->get_parameter('oauth_token');
+ $token = $this->data_store->lookup_token(
+ $consumer, $token_type, $token_field
+ );
+ if (!$token) {
+ throw new OAuthException("Invalid $token_type token: $token_field");
+ }
+ return $token;
+ }
+
+ /**
+ * all-in-one function to check the signature on a request
+ * should guess the signature method appropriately
+ */
+ private function check_signature(&$request, $consumer, $token) {
+ // this should probably be in a different method
+ $timestamp = @$request->get_parameter('oauth_timestamp');
+ $nonce = @$request->get_parameter('oauth_nonce');
+
+ $this->check_timestamp($timestamp);
+ $this->check_nonce($consumer, $token, $nonce, $timestamp);
+
+ $signature_method = $this->get_signature_method($request);
+
+ $signature = $request->get_parameter('oauth_signature');
+ $valid_sig = $signature_method->check_signature(
+ $request,
+ $consumer,
+ $token,
+ $signature
+ );
+
+ if (!$valid_sig) {
+ throw new OAuthException("Invalid signature");
+ }
+ }
+
+ /**
+ * check that the timestamp is new enough
+ */
+ private function check_timestamp($timestamp) {
+ if( ! $timestamp )
+ throw new OAuthException(
+ 'Missing timestamp parameter. The parameter is required'
+ );
+
+ // verify that timestamp is recentish
+ $now = time();
+ if (abs($now - $timestamp) > $this->timestamp_threshold) {
+ throw new OAuthException(
+ "Expired timestamp, yours $timestamp, ours $now"
+ );
+ }
+ }
+
+ /**
+ * check that the nonce is not repeated
+ */
+ private function check_nonce($consumer, $token, $nonce, $timestamp) {
+ if( ! $nonce )
+ throw new OAuthException(
+ 'Missing nonce parameter. The parameter is required'
+ );
+
+ // verify that the nonce is uniqueish
+ $found = $this->data_store->lookup_nonce(
+ $consumer,
+ $token,
+ $nonce,
+ $timestamp
+ );
+ if ($found) {
+ throw new OAuthException("Nonce already used: $nonce");
+ }
+ }
+
+}
+
+class OAuthDataStore {
+ function lookup_consumer($consumer_key) {
+ // implement me
+ }
+
+ function lookup_token($consumer, $token_type, $token) {
+ // implement me
+ }
+
+ function lookup_nonce($consumer, $token, $nonce, $timestamp) {
+ // implement me
+ }
+
+ function new_request_token($consumer, $callback = null) {
+ // return a new token attached to this consumer
+ }
+
+ function new_access_token($token, $consumer, $verifier = null) {
+ // return a new access token attached to this consumer
+ // for the user associated with this token if the request token
+ // is authorized
+ // should also invalidate the request token
+ }
+
+}
+
+class OAuthUtil {
+ public static function urlencode_rfc3986($input) {
+ if (is_array($input)) {
+ return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
+ } else if (is_scalar($input)) {
+ return str_replace(
+ '+',
+ ' ',
+ str_replace('%7E', '~', rawurlencode($input))
+ );
+ } else {
+ return '';
+ }
+}
+
+
+ // This decode function isn't taking into consideration the above
+ // modifications to the encoding process. However, this method doesn't
+ // seem to be used anywhere so leaving it as is.
+ public static function urldecode_rfc3986($string) {
+ return urldecode($string);
+ }
+
+ // Utility function for turning the Authorization: header into
+ // parameters, has to do some unescaping
+ // Can filter out any non-oauth parameters if needed (default behaviour)
+ public static function split_header($header, $only_allow_oauth_parameters = true) {
+ $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
+ $offset = 0;
+ $params = array();
+ while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
+ $match = $matches[0];
+ $header_name = $matches[2][0];
+ $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
+ if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) {
+ $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content);
+ }
+ $offset = $match[1] + strlen($match[0]);
+ }
+
+ if (isset($params['realm'])) {
+ unset($params['realm']);
+ }
+
+ return $params;
+ }
+
+ // helper to try to sort out headers for people who aren't running apache
+ public static function get_headers() {
+ if (function_exists('apache_request_headers')) {
+ // we need this to get the actual Authorization: header
+ // because apache tends to tell us it doesn't exist
+ $headers = apache_request_headers();
+
+ // sanitize the output of apache_request_headers because
+ // we always want the keys to be Cased-Like-This and arh()
+ // returns the headers in the same case as they are in the
+ // request
+ $out = array();
+ foreach( $headers AS $key => $value ) {
+ $key = str_replace(
+ " ",
+ "-",
+ ucwords(strtolower(str_replace("-", " ", $key)))
+ );
+ $out[$key] = $value;
+ }
+ } else {
+ // otherwise we don't have apache and are just going to have to hope
+ // that $_SERVER actually contains what we need
+ $out = array();
+ if( isset($_SERVER['CONTENT_TYPE']) )
+ $out['Content-Type'] = $_SERVER['CONTENT_TYPE'];
+ if( isset($_ENV['CONTENT_TYPE']) )
+ $out['Content-Type'] = $_ENV['CONTENT_TYPE'];
+
+ foreach ($_SERVER as $key => $value) {
+ if (substr($key, 0, 5) == "HTTP_") {
+ // this is chaos, basically it is just there to capitalize the first
+ // letter of every word that is not an initial HTTP and strip HTTP
+ // code from przemek
+ $key = str_replace(
+ " ",
+ "-",
+ ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
+ );
+ $out[$key] = $value;
+ }
+ }
+ }
+ return $out;
+ }
+
+ // This function takes a input like a=b&a=c&d=e and returns the parsed
+ // parameters like this
+ // array('a' => array('b','c'), 'd' => 'e')
+ public static function parse_parameters( $input ) {
+ if (!isset($input) || !$input) return array();
+
+ $pairs = explode('&', $input);
+
+ $parsed_parameters = array();
+ foreach ($pairs as $pair) {
+ $split = explode('=', $pair, 2);
+ $parameter = OAuthUtil::urldecode_rfc3986($split[0]);
+ $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';
+
+ if (isset($parsed_parameters[$parameter])) {
+ // We have already recieved parameter(s) with this name, so add to the list
+ // of parameters with this name
+
+ if (is_scalar($parsed_parameters[$parameter])) {
+ // This is the first duplicate, so transform scalar (string) into an array
+ // so we can add the duplicates
+ $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
+ }
+
+ $parsed_parameters[$parameter][] = $value;
+ } else {
+ $parsed_parameters[$parameter] = $value;
+ }
+ }
+ return $parsed_parameters;
+ }
+
+ public static function build_http_query($params) {
+ if (!$params) return '';
+
+ // Urlencode both keys and values
+ $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
+ $values = OAuthUtil::urlencode_rfc3986(array_values($params));
+ $params = array_combine($keys, $values);
+
+ // Parameters are sorted by name, using lexicographical byte value ordering.
+ // Ref: Spec: 9.1.1 (1)
+ uksort($params, 'strcmp');
+
+ $pairs = array();
+ foreach ($params as $parameter => $value) {
+ if (is_array($value)) {
+ // If two or more parameters share the same name, they are sorted by their value
+ // Ref: Spec: 9.1.1 (1)
+ natsort($value);
+ foreach ($value as $duplicate_value) {
+ $pairs[] = $parameter . '=' . $duplicate_value;
+ }
+ } else {
+ $pairs[] = $parameter . '=' . $value;
+ }
+ }
+ // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
+ // Each name-value pair is separated by an '&' character (ASCII code 38)
+ return implode('&', $pairs);
+ }
+}
+
diff --git a/app/modules/twitter/libraries/twitteroauth.php b/app/modules/twitter/libraries/twitteroauth.php
new file mode 100644
index 00000000..36c730f4
--- /dev/null
+++ b/app/modules/twitter/libraries/twitteroauth.php
@@ -0,0 +1,245 @@
+http_status; }
+ function lastAPICall() { return $this->last_api_call; }
+
+ /**
+ * construct TwitterOAuth object
+ */
+ function __construct($consumer_key, $consumer_secret, $oauth_token = NULL, $oauth_token_secret = NULL) {
+ $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1();
+ $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret);
+ if (!empty($oauth_token) && !empty($oauth_token_secret)) {
+ $this->token = new OAuthConsumer($oauth_token, $oauth_token_secret);
+ } else {
+ $this->token = NULL;
+ }
+ }
+
+
+ /**
+ * Get a request_token from Twitter
+ *
+ * @returns a key/value array containing oauth_token and oauth_token_secret
+ */
+ function getRequestToken($oauth_callback = NULL) {
+ $parameters = array();
+ if (!empty($oauth_callback)) {
+ $parameters['oauth_callback'] = $oauth_callback;
+ }
+ $request = $this->oAuthRequest($this->requestTokenURL(), 'GET', $parameters);
+ $token = OAuthUtil::parse_parameters($request);
+ $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
+ return $token;
+ }
+
+ /**
+ * Get the authorize URL
+ *
+ * @returns a string
+ */
+ function getAuthorizeURL($token, $sign_in_with_twitter = TRUE) {
+ if (is_array($token)) {
+ $token = $token['oauth_token'];
+ }
+ if (empty($sign_in_with_twitter)) {
+ return $this->authorizeURL() . "?oauth_token={$token}";
+ } else {
+ return $this->authenticateURL() . "?oauth_token={$token}";
+ }
+ }
+
+ /**
+ * Exchange request token and secret for an access token and
+ * secret, to sign API calls.
+ *
+ * @returns array("oauth_token" => "the-access-token",
+ * "oauth_token_secret" => "the-access-secret",
+ * "user_id" => "9436992",
+ * "screen_name" => "abraham")
+ */
+ function getAccessToken($oauth_verifier = FALSE) {
+ $parameters = array();
+ if (!empty($oauth_verifier)) {
+ $parameters['oauth_verifier'] = $oauth_verifier;
+ }
+ $request = $this->oAuthRequest($this->accessTokenURL(), 'GET', $parameters);
+ $token = OAuthUtil::parse_parameters($request);
+ $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
+ return $token;
+ }
+
+ /**
+ * One time exchange of username and password for access token and secret.
+ *
+ * @returns array("oauth_token" => "the-access-token",
+ * "oauth_token_secret" => "the-access-secret",
+ * "user_id" => "9436992",
+ * "screen_name" => "abraham",
+ * "x_auth_expires" => "0")
+ */
+ function getXAuthToken($username, $password) {
+ $parameters = array();
+ $parameters['x_auth_username'] = $username;
+ $parameters['x_auth_password'] = $password;
+ $parameters['x_auth_mode'] = 'client_auth';
+ $request = $this->oAuthRequest($this->accessTokenURL(), 'POST', $parameters);
+ $token = OAuthUtil::parse_parameters($request);
+ $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
+ return $token;
+ }
+
+ /**
+ * GET wrapper for oAuthRequest.
+ */
+ function get($url, $parameters = array()) {
+ $response = $this->oAuthRequest($url, 'GET', $parameters);
+ if ($this->format === 'json' && $this->decode_json) {
+ return json_decode($response);
+ }
+ return $response;
+ }
+
+ /**
+ * POST wrapper for oAuthRequest.
+ */
+ function post($url, $parameters = array()) {
+ $response = $this->oAuthRequest($url, 'POST', $parameters);
+ if ($this->format === 'json' && $this->decode_json) {
+ return json_decode($response);
+ }
+ return $response;
+ }
+
+ /**
+ * DELETE wrapper for oAuthReqeust.
+ */
+ function delete($url, $parameters = array()) {
+ $response = $this->oAuthRequest($url, 'DELETE', $parameters);
+ if ($this->format === 'json' && $this->decode_json) {
+ return json_decode($response);
+ }
+ return $response;
+ }
+
+ /**
+ * Format and sign an OAuth / API request
+ */
+ function oAuthRequest($url, $method, $parameters) {
+ if (strrpos($url, 'https://') !== 0 && strrpos($url, 'http://') !== 0) {
+ $url = "{$this->host}{$url}.{$this->format}";
+ }
+ $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $parameters);
+ $request->sign_request($this->sha1_method, $this->consumer, $this->token);
+ switch ($method) {
+ case 'GET':
+ return $this->http($request->to_url(), 'GET');
+ default:
+ return $this->http($request->get_normalized_http_url(), $method, $request->to_postdata());
+ }
+ }
+
+ /**
+ * Make an HTTP request
+ *
+ * @return API results
+ */
+ function http($url, $method, $postfields = NULL) {
+ $this->http_info = array();
+ $ci = curl_init();
+ /* Curl settings */
+ curl_setopt($ci, CURLOPT_USERAGENT, $this->useragent);
+ curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, $this->connecttimeout);
+ curl_setopt($ci, CURLOPT_TIMEOUT, $this->timeout);
+ curl_setopt($ci, CURLOPT_RETURNTRANSFER, TRUE);
+ curl_setopt($ci, CURLOPT_HTTPHEADER, array('Expect:'));
+ curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, $this->ssl_verifypeer);
+ curl_setopt($ci, CURLOPT_HEADERFUNCTION, array($this, 'getHeader'));
+ curl_setopt($ci, CURLOPT_HEADER, FALSE);
+
+ switch ($method) {
+ case 'POST':
+ curl_setopt($ci, CURLOPT_POST, TRUE);
+ if (!empty($postfields)) {
+ curl_setopt($ci, CURLOPT_POSTFIELDS, $postfields);
+ }
+ break;
+ case 'DELETE':
+ curl_setopt($ci, CURLOPT_CUSTOMREQUEST, 'DELETE');
+ if (!empty($postfields)) {
+ $url = "{$url}?{$postfields}";
+ }
+ }
+
+ curl_setopt($ci, CURLOPT_URL, $url);
+ $response = curl_exec($ci);
+ $this->http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
+ $this->http_info = array_merge($this->http_info, curl_getinfo($ci));
+ $this->url = $url;
+ curl_close ($ci);
+ return $response;
+ }
+
+ /**
+ * Get the header info to store.
+ */
+ function getHeader($ch, $header) {
+ $i = strpos($header, ':');
+ if (!empty($i)) {
+ $key = str_replace('-', '_', strtolower(substr($header, 0, $i)));
+ $value = trim(substr($header, $i + 2));
+ $this->http_header[$key] = $value;
+ }
+ return strlen($header);
+ }
+}
diff --git a/app/modules/twitter/models/bitly_model.php b/app/modules/twitter/models/bitly_model.php
new file mode 100644
index 00000000..e4caf641
--- /dev/null
+++ b/app/modules/twitter/models/bitly_model.php
@@ -0,0 +1,71 @@
+getCreds();
+
+ if($creds == false){
+ return false;
+ }
+
+ if(!preg_match('/^http/',$url)){
+ $url = 'http://' . $url;
+ }
+
+ $url = urlencode($url);
+
+ $c = curl_init();
+ $bitly = "http://api.bit.ly/v3/shorten";
+ $uriParams = "login=" . $creds['bitly_id'] . "&apikey=" . $creds['bitly_api_key'] . "&longUrl=" . $url . "&format=txt";
+ curl_setopt($c, CURLOPT_URL, $bitly . '?'. $uriParams);
+ curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($c, CURLOPT_SSL_VERIFYPEER, false);
+ $returnData = curl_exec ($c);
+ curl_close ($c);
+
+ $shortUrl = trim($returnData);
+
+ if(preg_match('/^http/', $shortUrl)){
+ return $shortUrl;
+ }
+ return false;
+ }
+
+ function getCreds(){
+ $this->load->model('settings/settings_model', 'settings');
+
+ $settings = $this->settings->get_setting('bitly_id');
+ $results['bitly_id'] = $settings['value'];
+ $settings = $this->settings->get_setting('bitly_api_key');
+ $results['bitly_api_key'] = $settings['value'];
+
+ if(empty($results['bitly_id']))
+ {
+ $results = false;
+ } elseif (empty($results['bitly_api_key'])){
+ $results = false;
+ }
+ return $results;
+ }
+}
+?>
\ No newline at end of file
diff --git a/app/modules/twitter/models/twitter_model.php b/app/modules/twitter/models/twitter_model.php
new file mode 100644
index 00000000..9519d42a
--- /dev/null
+++ b/app/modules/twitter/models/twitter_model.php
@@ -0,0 +1,233 @@
+settings_model->update_setting('twitter_last_tweet', date('Y-m-d H:i:s'));
+
+ return FALSE;
+ }
+
+ if (setting('twitter_content_types') == '') {
+ cron_log('No content types have been configured for tweeting. Exiting.');
+
+ $this->settings_model->update_setting('twitter_last_tweet', date('Y-m-d H:i:s'));
+ return FALSE;
+ }
+
+ // load libraries
+ $CI =& get_instance();
+
+ $CI->load->model('publish/content_model');
+ require(APPPATH . 'modules/twitter/libraries/twitteroauth.php');
+
+ // only process last hour
+ $start_date = date('Y-m-d', strtotime('now - 1 hour'));
+
+ $types = unserialize(setting('twitter_content_types'));
+ $topics = (setting('twitter_topics') == '') ? NULL : unserialize(setting('twitter_topics'));
+
+ // if they have all topics...
+ if ((is_int($topics) && $topics = 0) || (is_array($topics) && in_array(0, $topics))) {
+ $topics = NULL;
+ }
+
+ foreach ($types as $type) {
+ $filter = array(
+ 'type' => $type
+ ,'start_date' => $start_date
+ ,'limit' => '50'
+ );
+
+ if ((is_int($topics) && $topics != 0) || (is_array($topics) && !in_array(0, $topics))) {
+ $filter['topics'] = $topics;
+ }
+
+ $contents = $CI->content_model->get_contents($filter);
+
+
+ if (!empty($contents)) {
+ // flip so that the latest posts are tweeted last
+ $contents = array_reverse($contents);
+
+ foreach ($contents as $content) {
+ // have we already tweeted this?
+ if (
+ $this->db->select('link_id')
+ ->from('links')
+ ->where('link_module','twitter')
+ ->where('link_parameter',$content['link_id'])
+ ->get()
+ ->num_rows() > 0
+ ) {
+
+ continue;
+ }
+
+ if (!isset($connection)) {
+ $connection = new TwitterOAuth(setting('twitter_consumer_key'), setting('twitter_consumer_secret'), setting('twitter_oauth_token'), setting('twitter_oauth_token_secret'));
+
+ cron_log('Connected to Twitter via OAuth.');
+ }
+
+ // build $status
+
+ // shorten URL
+ $CI->load->model('link_model');
+ $CI->load->helper('string');
+ $string = random_string('alnum',5);
+
+ // make sure it's unique
+ $url_path = $CI->link_model->get_unique_url_path($string);
+
+ $url = site_url($url_path);
+
+ $this->load->model('twitter/bitly_model','bitly');
+ $bitlyUrl = $this->bitly->shorten_url($url);
+ if($bitlyUrl){
+ $url = $bitlyUrl;
+ }
+
+ // start with URL
+ $status = $url;
+
+ // how many characters remain?
+ $chars_remain = 140 - strlen($status);
+
+ // shorten title to fit before link
+ $CI->load->helper('shorten');
+ $shortened_title = shorten($content['title'], ($chars_remain - 5), FALSE);
+ $shortened_title = str_replace('&hellip','...',$shortened_title);
+
+ $status = $shortened_title . ' ' . $status;
+
+ // insert into links table
+ $CI->link_model->new_link($url_path, FALSE, $content['title'], 'Twitter Link', 'twitter', 'twitter', 'redirect', $content['link_id']);
+
+ //insert tweet content into tweets_sent
+ $this->twitter_log($status, $content['id'], $type);
+
+ cron_log('Posting status: ' . $status);
+
+ $result = $connection->post('statuses/update', array('status' => $status));
+
+ if ($connection->http_code != 200) {
+ cron_log('Connection to Twitter failed. Exiting.');
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ // update cron run
+ $this->settings_model->update_setting('twitter_last_tweet', date('Y-m-d H:i:s'));
+
+ return TRUE;
+ }
+
+ /**
+ * Post the twitter status update into the db to keep a log of it
+ */
+ public function twitter_log($tweet, $content_id, $content_type){
+ $data = array(
+ 'tweet' => $tweet
+ ,'content_id' => $content_id
+ ,'content_type' => $content_type
+ ,'sent_time' => date('Y-m-d H:i:s', time())
+ );
+ $this->db->insert('tweets_sent', $data);
+ }
+
+ /**
+ * Get Tweets
+ *
+ * @param int $filters['id'] The tweet ID to select
+ * @param string $filters['tweet'] Search by tweet
+ * @param date $filters['start_date'] Select after this tweet date
+ * @param date $filters['end_date'] Select before this tweet date
+ * @param string $filters['sort'] Field to sort by
+ * @param string $filters['sort_dir'] ASC or DESC
+ * @param int $filters['limit'] How many records to retrieve
+ * @param int $filters['offset'] Start records retrieval at this record
+ * @param boolean $counting Should we just count the number of tweets that match the filters? (default: FALSE)
+ *
+ * @return array Each tweet in an array of tweets
+ */
+ function get_tweets ($filters = array(), $counting = FALSE) {
+ // filters
+ if (isset($filters['id'])) {
+ $this->db->where('tweet_id',$filters['id']);
+ }
+
+ if (isset($filters['tweet'])) {
+ $this->db->like('tweet',$filters['tweet']);
+ }
+
+ if (isset($filters['start_date'])) {
+ $date = date('Y-m-d H:i:s', strtotime($filters['start_date']));
+ $this->db->where('sent_time >=', $date);
+ }
+
+ if (isset($filters['end_date'])) {
+ $date = date('Y-m-d H:i:s', strtotime($filters['end_date']));
+ $this->db->where('sent_time <=', $date);
+ }
+
+ // standard ordering and limiting
+ if ($counting == FALSE) {
+ $order_by = (isset($filters['sort'])) ? $filters['sort'] : 'tweet_id';
+ $order_dir = (isset($filters['sort_dir'])) ? $filters['sort_dir'] : 'ASC';
+ $this->db->order_by($order_by, $order_dir);
+
+ if (isset($filters['limit'])) {
+ $offset = (isset($filters['offset'])) ? $filters['offset'] : 0;
+ $this->db->limit($filters['limit'], $offset);
+ }
+ }
+
+ if ($counting === TRUE) {
+ $this->db->select('tweet_id');
+ $result = $this->db->get('tweets_sent');
+ $rows = $result->num_rows();
+ $result->free_result();
+ return $rows;
+ }
+ else {
+ $this->db->from('tweets_sent');
+
+ $result = $this->db->get();
+ }
+
+ if ($result->num_rows() == 0) {
+ return FALSE;
+ }
+
+ // get custom fields
+ $CI =& get_instance();
+ $CI->load->model('custom_fields_model');
+ $custom_fields = $CI->custom_fields_model->get_custom_fields(array('group' => '1'));
+
+ return $result->result_array();
+ }
+}
\ No newline at end of file
diff --git a/app/modules/twitter/twitter.php b/app/modules/twitter/twitter.php
new file mode 100644
index 00000000..022eef08
--- /dev/null
+++ b/app/modules/twitter/twitter.php
@@ -0,0 +1,80 @@
+active_module = $this->name;
+
+ parent::__construct();
+ }
+
+ /*
+ * Pre-admin function
+ *
+ * Initiate navigation in control panel
+ */
+ function admin_preload ()
+ {
+ $this->CI->admin_navigation->child_link('configuration',60,'Twitter',site_url('admincp/twitter'));
+ $this->CI->admin_navigation->child_link('reports',91,'Tweets Sent',site_url('admincp/twitter/tweet_logs'));
+ }
+
+ /*
+ * Module update
+ *
+ * @param int $db_version The current DB version
+ *
+ * @return int The current software version, to update the database
+ */
+ function update ($db_version) {
+ if ($db_version < '1.01') {
+ $this->CI->db->query('CREATE TABLE `tweets_sent` (
+ `tweet_id` int(11) NOT NULL auto_increment,
+ `tweet` varchar(255) NOT NULL,
+ `sent_time` DATETIME,
+ `content_id` int(11) NOT NULL,
+ `content_type` int(11) NOT NULL,
+ PRIMARY KEY (`tweet_id`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;');
+ }
+ if($db_version < '1.01'){
+ // initial install
+ $this->CI->settings_model->new_setting(1, 'bitly_id', '', '', 'text','', FALSE, FALSE);
+ $this->CI->settings_model->new_setting(1, 'bitly_api_key', '', '', 'text','', FALSE, FALSE);
+ }
+ if ($db_version < '1.01') {
+
+ // initial install
+ $this->CI->settings_model->new_setting(1, 'twitter_consumer_key', '', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_consumer_secret', '', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_oauth_token', '', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_oauth_token_secret', '', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_content_types', '', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_topics', '', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_template', '', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_enabled', '0', '', 'text','', FALSE, TRUE);
+ $this->CI->settings_model->new_setting(1, 'twitter_last_tweet', '', '', 'text','', FALSE, TRUE);
+ }
+
+ if ($db_version < '1.01') {
+ $this->CI->app_hooks->bind('cron','Twitter_model','hook_cron',APPPATH . 'modules/twitter/models/twitter_model.php');
+ }
+
+ // return current version
+ return $this->version;
+ }
+}
\ No newline at end of file
diff --git a/app/modules/twitter/views/generic.php b/app/modules/twitter/views/generic.php
new file mode 100644
index 00000000..89edf607
--- /dev/null
+++ b/app/modules/twitter/views/generic.php
@@ -0,0 +1,11 @@
+=$this->load->view(branded_view('cp/header'));?>
+=$form_title;?>
+
+
+=$form;?>
+
+
+
+
+
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/twitter/views/tweets.php b/app/modules/twitter/views/tweets.php
new file mode 100644
index 00000000..8f605f5d
--- /dev/null
+++ b/app/modules/twitter/views/tweets.php
@@ -0,0 +1,23 @@
+=$this->load->view(branded_view('cp/header'));?>
+Tweet Records
+=$this->dataset->table_head();?>
+
+if (!empty($this->dataset->data)) {
+ foreach ($this->dataset->data as $row) {
+ ?>
+
+ =$row['tweet_id'];?>
+ =$row['sent_time'];?>
+ =$row['tweet'];?>
+
+
+ }
+}
+else {
+?>
+
+ No tweets match your filters.
+
+ } ?>
+=$this->dataset->table_close();?>
+=$this->load->view(branded_view('cp/footer'));?>
\ No newline at end of file
diff --git a/app/modules/users/models/user_model.php b/app/modules/users/models/user_model.php
index 072473ba..86a33572 100644
--- a/app/modules/users/models/user_model.php
+++ b/app/modules/users/models/user_model.php
@@ -153,9 +153,9 @@ public function login ($username, $password, $remember = FALSE) {
$user_db = $query->row_array();
$user = $this->get_user($user_db['user_id']);
- $hashed_password = ($user['salt'] == '') ? md5($password) : md5($password . ':' . $user['salt']);
+ $hashPassSalt = ($user['salt'] == '') ? $this->generatePassSalt($password,'') : $this->generatePassSalt($password,$user['salt']));
- if ($hashed_password == $user_db['user_password']) {
+ if ($hashPassSalt->pass == $user_db['user_password']) {
$authenticated = TRUE;
}
}
@@ -940,10 +940,7 @@ function new_user($email, $password, $username, $first_name, $last_name, $groups
}
// generate hashed password
- $CI =& get_instance();
- $CI->load->helper('string');
- $salt = random_string('unique');
- $hashed_password = md5($password . ':' . $salt);
+ $hashPassSalt = $this->generatePassSalt($password);
$insert_fields = array(
'user_is_admin' => ($is_admin == TRUE) ? '1' : '0',
@@ -952,8 +949,8 @@ function new_user($email, $password, $username, $first_name, $last_name, $groups
'user_last_name' => $last_name,
'user_username' => $username,
'user_email' => $email,
- 'user_password' => $hashed_password,
- 'user_salt' => $salt,
+ 'user_password' => $hashPassSalt->pass,
+ 'user_salt' => $hashPassSalt->salt,
'user_referrer' => ($affiliate != FALSE) ? $affiliate : '0',
'user_signup_date' => date('Y-m-d H:i:s'),
'user_last_login' => '0000-00-00 00:00:00',
@@ -974,6 +971,7 @@ function new_user($email, $password, $username, $first_name, $last_name, $groups
// create customer record
if (module_installed('billing')) {
+ $CI =& get_instance();
$CI->load->model('billing/customer_model');
$customer = array();
@@ -1196,12 +1194,9 @@ function delete_user ($user_id) {
* @return boolean
*/
function update_password ($user_id, $new_password) {
- $CI =& get_instance();
- $CI->load->helper('string');
- $salt = random_string('unique');
- $hashed_password = md5($new_password . ':' . $salt);
+ $hashPassSalt = $this->generatePassSalt($new_password);
- $this->db->update('users',array('user_password' => $hashed_password, 'user_salt' => $salt),array('user_id' => $user_id));
+ $this->db->update('users',array('user_password' => $hashPassSalt->pass, 'user_salt' => $hashPassSalt->salt),array('user_id' => $user_id));
// prep hook
$CI =& get_instance();
@@ -1231,7 +1226,8 @@ function reset_password ($user_id) {
$this->load->helper('string');
$password = random_string('alnum',9);
- $this->db->update('users',array('user_password' => md5($password), 'user_salt' => ''),array('user_id' => $user['id']));
+ $hashPassSalt = $this->generatePassSalt($password,'');
+ $this->db->update('users',array('user_password' => $hashPassSalt->pass, 'user_salt' => ''),array('user_id' => $user['id']));
// hook call
$CI =& get_instance();
@@ -1244,6 +1240,25 @@ function reset_password ($user_id) {
return TRUE;
}
+ /**
+ * Generate a hashed password and unique salt
+ *
+ * @param string $password the password enetered by the user
+ * @return array an array containing the password and salt for a user
+ */
+ function generatePassSalt($password,$salt=null){
+ // generate hashed password
+ if($salt == '' ) {
+ $hashed_password = md5($password);
+ } else {
+ if($salt == null){
+ $salt = uniqid();
+ }
+ $hashed_password = md5($password . ':' . $salt);
+ }
+ return array('pass'=>$hashed_password,'salt'=>$salt);
+ }
+
/**
* Suspend User
*
diff --git a/branding/default/js/ckeditor/config.js b/branding/default/js/ckeditor/config.js
index d63df947..6c4511f2 100755
--- a/branding/default/js/ckeditor/config.js
+++ b/branding/default/js/ckeditor/config.js
@@ -10,7 +10,8 @@ CKEDITOR.editorConfig = function( config ) {
config.extraPlugins = 'font,panelbutton,colorbutton,justify,menubutton,scayt';
config.scayt_autoStartup = true;
-
+ config.disableNativeSpellChecker = false;
+
// The toolbar groups arrangement, optimized for two toolbar rows.
config.toolbarGroups = [
{ name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
diff --git a/branding/default/js/datePicker.js b/branding/default/js/datePicker.js
index 3a5a402f..7cc3ef7a 100644
--- a/branding/default/js/datePicker.js
+++ b/branding/default/js/datePicker.js
@@ -1159,7 +1159,7 @@
selectWeek : false,
verticalPosition : $.dpConst.POS_TOP,
horizontalPosition : $.dpConst.POS_LEFT,
- verticalOffset : 0,
+ verticalOffset : '20px',
horizontalOffset : 0,
hoverClass : 'dp-hover'
};
diff --git a/branding/default/js/universal.js b/branding/default/js/universal.js
index 54bd23dc..4ab982d7 100644
--- a/branding/default/js/universal.js
+++ b/branding/default/js/universal.js
@@ -51,7 +51,7 @@ $(document).ready(function() {
url += $('input#title').val().toLowerCase();
- url = url.replace(/\s+/g,'_');
+ url = url.replace(/\s+/g,'-');
url = url.replace(/<(.*?)>/g, '');
url = url.replace(/\/{2,10}/g,'');
url = url.replace(/[^a-z0-9\/\-_\.]/ig,'');
diff --git a/docs/designers/reference/menus.md b/docs/designers/reference/menus.md
index e77be95f..cee0a81d 100644
--- a/docs/designers/reference/menus.md
+++ b/docs/designers/reference/menus.md
@@ -32,7 +32,7 @@ The following menu is generated with `{menu}` tag documented below:
Contact Us
-
+
-
via Email
@@ -57,7 +57,7 @@ In order to give you the best opportunities for styling this menu with *CSS styl
## Child Sub-menus
-If you have created a sub-menu for a link item [when creating your menu in the control panel](/docs/publishing/menus.md), these will be exported as embedded `` lists with a class of "children".
+If you have created a sub-menu for a link item [when creating your menu in the control panel](/docs/publishing/menus.md), these will be exported as embedded `` lists with a class of "children". To add another class to the ul use {menu child_class="the_name_of_class_to_use"}. You can insert multiple classes here separated by spaces.
If you want to show these as a dropdown menu, you will need to hide all `ul.children` elements and then use JavaScript to show the sub-menus when hovering over the main link.
@@ -110,6 +110,6 @@ Example usage (to return the menu HTML code used in the example at the top of th
```
```
\ No newline at end of file
diff --git a/hero-commercial-license.txt b/hero-commercial-license.txt
new file mode 100644
index 00000000..e4e6ddc7
--- /dev/null
+++ b/hero-commercial-license.txt
@@ -0,0 +1,92 @@
+THIS LICENSE APPLIES TO THE OPTIONAL HERO ECOMMERCE ADD-ON WITH COMPONENTS AT:
+
+* /app/modules/billing
+* /app/modules/store
+* /app/modules/coupons
+
+YOUR USE OF THIS SOFTWARE IS GOVERNED BY THE TERMS AND CONDITIONS SET FORTH IN
+THIS LICENSE.
+
+Last Revised: 14 December, 2010
+
+This license is a legal agreement between you and Electric Function, Inc. for the use of Hero Software (the
+"Software"). By downloading Hero or using it in any capacity, you agree to be bound by the terms and conditions of this
+license. Electric Function, Inc. reserves the right to alter this agreement at any time, for any reason, without notice.
+
+RESTRICTIONS
+
+Unless you have been granted prior, written consent from Electric Function, Inc., you may not:
+
+* Use your software/license on more than a single domain and staging/development environment.
+* Reproduce, distribute, or transfer the Software, or portions thereof, to any third party.
+* Sell, rent, lease, assign, or sublet the Software or portions thereof.
+* Grant rights to any other person.
+* Use the Software in violation of any Canadian or international law or regulation.
+
+LICENSING MECHANISM
+
+This software uses IonCube (http://www.ioncube.com) technology to limit the use of the software to a single IP address
+or domain name. In the unforeseen circumstance that Electric Function, Inc. ceases to exist and its servers are
+dysfunctional, your software will continue to function without issue for as long as these are conditions are met. If
+you move the software to a server that does not share this domain or IP address, the software will cease to function
+and you will experience a license error until the issue is resolved.
+
+In order to protect the copyright of this software, certain files deemed necessary for the proper function of the
+application are encoded with IonCube technology. This prevents bypassing the licensing mechanism and protects Electric
+Function, Inc.'s copyright and intellectual property. However, unencoded "open source" files in the application are
+still copyrighted by Electric Function, Inc. (unless otherwise specified) and are subject to all of the terms and
+conditions of this agreement.
+
+DISPLAY OF COPYRIGHT NOTICES
+
+All copyright and proprietary notices within the Software files must remain intact. You are free to re-brand the
+frontend and control panel layouts without any mention of Hero or Electric Function, Inc., however attribution of the
+copyright to Electric Function, Inc. must remain visible in the footer of the control panel.
+
+MAKING BACKUP/DEVELOPMENT COPIES
+
+You may make copies of the Software for back-up purposes or for a development/staging environment, provided that you
+reproduce the Software in its original form and with all proprietary notices on the back-up copy. Licenses will be
+issued for development/staging environments at Electric Function, Inc.'s discretion.
+
+SOFTWARE MODIFICATION
+
+You may alter, modify, or extend the Software for your own use, or commission a third-party to perform modifications
+for you, but you may not resell, redistribute or transfer the modified or derivative version without prior written
+consent from Electric Function, Inc. Components from the Software may not be extracted and used in other programs
+without prior written consent from Electric Function, Inc.
+
+You may distribute, resell, extract, and re-use original modules developed by you that exist in the "modules" folder
+and reference Hero API's as long as they do not include the original source of the API's in their distributed form.
+
+TECHNICAL SUPPORT
+
+Technical support is available through online forums and support tickets. Phone support may be available upon request,
+but this is not guaranteed unless specified in an additional support contract. No representations or guarantees are
+made regarding the response time in which support questions are answered.
+
+REFUNDS
+
+Electric Function, Inc. offer a 30-Day, money back guarantee on purchases of Hero, barring a breach of the terms and
+conditions put forth in this agreement. If you are unsatisfied with Hero or it is no longer a fit for your project
+within 30 days of purchase, just submit a support ticket to Electric Function, Inc. for a full refund.
+
+INDEMNITY
+
+You agree to indemnify and hold harmless Electric Function, Inc. for any claims, actions or suits, as well as any
+related expenses, liabilities, damages, settlements or fees arising from your use or misuse of the Software, or a
+violation of any terms of this license.
+
+DISCLAIMER OF WARRANTY
+
+This software is provided "as is", without warranty of any kind, expressed or implied, including, but not limited to,
+warranties of quality, performance, non-infringement, merchantability, or fitness for a particular purpose. Further,
+Electric Function, Inc. does not warrant that the software or any related service will always be available.
+
+LIMITATIONS OF LIABILITY
+
+You assume all risk associated with the installation and use of the software. In no event shall the authors or
+copyright holders of the software be liable for claims, damages, or other liability arising from, out of, or in
+connection with the software. License holders are solely responsible for determining the appropriateness of use and
+assume all risks associated with its use, including but not limited to the risks of program errors, damage to
+equipment, loss of data or software programs, or unavailability or interruption of operations.
\ No newline at end of file
diff --git a/themes/cubed/css/gallery.css b/themes/cubed/css/gallery.css
new file mode 100644
index 00000000..3f24209f
--- /dev/null
+++ b/themes/cubed/css/gallery.css
@@ -0,0 +1,21 @@
+ul.gallery_images {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+ul.gallery_images li {
+ display: block;
+ float: left;
+ height: 100px;
+ padding: 15px;
+ width: 25%;
+}
+
+ul.gallery_images img {
+ border: 1px solid #666;
+}
+
+ul.gallery_images a:hover img {
+ border: 1px solid #222;
+}
\ No newline at end of file
diff --git a/themes/cubed/error.thtml b/themes/cubed/error.thtml
new file mode 100644
index 00000000..1058c671
--- /dev/null
+++ b/themes/cubed/error.thtml
@@ -0,0 +1,6 @@
+{extends file="layout.thtml"}
+{block name="title"}{$title} - {$smarty.block.parent}{/block}
+{block name="content"}
+ {$title}
+ {$message}
+{/block}
\ No newline at end of file
diff --git a/themes/cubed/gallery.thtml b/themes/cubed/gallery.thtml
new file mode 100644
index 00000000..81dd5536
--- /dev/null
+++ b/themes/cubed/gallery.thtml
@@ -0,0 +1,28 @@
+{extends file="layout.thtml"}
+{block name="title"}
+{$title} - {$smarty.block.parent}
+{/block}
+{block name="head_includes"}
+
+
+
+
+
+{/block}
+{block name="content"}
+ {$title}
+
+ Download this gallery as a zip archive
+
+
+{/block}
\ No newline at end of file
diff --git a/themes/cubed/shadowbox/LICENSE b/themes/cubed/shadowbox/LICENSE
new file mode 100644
index 00000000..59e6eb67
--- /dev/null
+++ b/themes/cubed/shadowbox/LICENSE
@@ -0,0 +1,65 @@
+Copyright 2007-2010 Michael J. I. Jackson, all rights reserved.
+
+The following licenses are applicable to version 3.0 of the Shadowbox.js library
+and greater. The term "library" is used throughout to mean all code, images, and
+documentation that are distributed as part of the library.
+
+
+Shadowbox.js Non-Commercial License version 1.0
+
+Under the terms of this license, the licensee is granted the following privileges:
+
+ - The right to use the library as part of a website or application that is not
+ used for commercial purposes
+
+ - The right to modify the library to suit his purpose.
+
+Under no circumstance is the licensee permitted to redistribute the source code,
+images, or documentation contained in the library. All redistribution rights
+remain with the copyright holder unless specific prior written permission is
+obtained.
+
+Under no circumstance shall this copyright notice or list of conditions be
+modified or removed from the code distribution in either source or binary form.
+
+This software is provided by the copyright holder and contributors "as is" and
+any express or implied warranties, including, but not limited to, the implied
+warranties of merchantability and fitness for a particular purpose are disclaimed.
+
+In no event shall the copyright holder or contributors be liable for any direct,
+indirect, incidental, special, exemplary, or consequential damages (including, but
+not limited to, procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of liability,
+whether in contract, strict liability, or tort (including negligence or otherwise)
+arising in any way out of the use of this software, even if advised of the
+possibility of such damage.
+
+
+Shadowbox.js Commercial License version 1.0
+
+Under the terms of this license, the licensee is granted the following privileges:
+
+ - The right to use the library as part of a website or application that is
+ used for commercial purposes
+
+ - The right to modify the library to suit his purpose.
+
+Under no circumstance is the licensee permitted to redistribute the source code,
+images, or documentation contained in the library. All redistribution rights
+remain with the copyright holder unless specific prior written permission is
+obtained.
+
+Under no circumstance shall this copyright notice or list of conditions be
+modified or removed from the code distribution in either source or binary form.
+
+This software is provided by the copyright holder and contributors "as is" and
+any express or implied warranties, including, but not limited to, the implied
+warranties of merchantability and fitness for a particular purpose are disclaimed.
+
+In no event shall the copyright holder or contributors be liable for any direct,
+indirect, incidental, special, exemplary, or consequential damages (including, but
+not limited to, procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of liability,
+whether in contract, strict liability, or tort (including negligence or otherwise)
+arising in any way out of the use of this software, even if advised of the
+possibility of such damage.
diff --git a/themes/cubed/shadowbox/close.png b/themes/cubed/shadowbox/close.png
new file mode 100644
index 00000000..33c1aab5
Binary files /dev/null and b/themes/cubed/shadowbox/close.png differ
diff --git a/themes/cubed/shadowbox/loading.gif b/themes/cubed/shadowbox/loading.gif
new file mode 100644
index 00000000..fac5a1b1
Binary files /dev/null and b/themes/cubed/shadowbox/loading.gif differ
diff --git a/themes/cubed/shadowbox/next.png b/themes/cubed/shadowbox/next.png
new file mode 100644
index 00000000..0c950d6a
Binary files /dev/null and b/themes/cubed/shadowbox/next.png differ
diff --git a/themes/cubed/shadowbox/pause.png b/themes/cubed/shadowbox/pause.png
new file mode 100644
index 00000000..0b5f804a
Binary files /dev/null and b/themes/cubed/shadowbox/pause.png differ
diff --git a/themes/cubed/shadowbox/play.png b/themes/cubed/shadowbox/play.png
new file mode 100644
index 00000000..d26c9330
Binary files /dev/null and b/themes/cubed/shadowbox/play.png differ
diff --git a/themes/cubed/shadowbox/previous.png b/themes/cubed/shadowbox/previous.png
new file mode 100644
index 00000000..f39220d8
Binary files /dev/null and b/themes/cubed/shadowbox/previous.png differ
diff --git a/themes/cubed/shadowbox/shadowbox.css b/themes/cubed/shadowbox/shadowbox.css
new file mode 100644
index 00000000..dfe7eb9c
--- /dev/null
+++ b/themes/cubed/shadowbox/shadowbox.css
@@ -0,0 +1,30 @@
+#sb-title-inner,#sb-info-inner,#sb-loading-inner,div.sb-message{font-family:"HelveticaNeue-Light","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:200;color:#fff;}
+#sb-container{position:fixed;margin:0;padding:0;top:0;left:0;z-index:999;text-align:left;visibility:hidden;display:none;}
+#sb-overlay{position:relative;height:100%;width:100%;}
+#sb-wrapper{position:absolute;visibility:hidden;width:100px;}
+#sb-wrapper-inner{position:relative;border:1px solid #303030;overflow:hidden;height:100px;}
+#sb-body{position:relative;height:100%;}
+#sb-body-inner{position:absolute;height:100%;width:100%;}
+#sb-player.html{height:100%;overflow:auto;}
+#sb-body img{border:none;}
+#sb-loading{position:relative;height:100%;}
+#sb-loading-inner{position:absolute;font-size:14px;line-height:24px;height:24px;top:50%;margin-top:-12px;width:100%;text-align:center;}
+#sb-loading-inner span{background:url(loading.gif) no-repeat;padding-left:34px;display:inline-block;}
+#sb-body,#sb-loading{background-color:#060606;}
+#sb-title,#sb-info{position:relative;margin:0;padding:0;overflow:hidden;}
+#sb-title,#sb-title-inner{height:26px;line-height:26px;}
+#sb-title-inner{font-size:16px;}
+#sb-info,#sb-info-inner{height:20px;line-height:20px;}
+#sb-info-inner{font-size:12px;}
+#sb-nav{float:right;height:16px;padding:2px 0;width:45%;}
+#sb-nav a{display:block;float:right;height:16px;width:16px;margin-left:3px;cursor:pointer;background-repeat:no-repeat;}
+#sb-nav-close{background-image:url(close.png);}
+#sb-nav-next{background-image:url(next.png);}
+#sb-nav-previous{background-image:url(previous.png);}
+#sb-nav-play{background-image:url(play.png);}
+#sb-nav-pause{background-image:url(pause.png);}
+#sb-counter{float:left;width:45%;}
+#sb-counter a{padding:0 4px 0 0;text-decoration:none;cursor:pointer;color:#fff;}
+#sb-counter a.sb-counter-current{text-decoration:underline;}
+div.sb-message{font-size:12px;padding:10px;text-align:center;}
+div.sb-message a:link,div.sb-message a:visited{color:#fff;text-decoration:underline;}
diff --git a/themes/cubed/shadowbox/shadowbox.js b/themes/cubed/shadowbox/shadowbox.js
new file mode 100644
index 00000000..6821a7fc
--- /dev/null
+++ b/themes/cubed/shadowbox/shadowbox.js
@@ -0,0 +1,17 @@
+/*
+ * Shadowbox.js, version 3.0.3
+ * http://shadowbox-js.com/
+ *
+ * Copyright 2007-2010, Michael J. I. Jackson
+ * Date: 2011-05-14 06:45:18 +0000
+ */
+(function(au,k){var Q={version:"3.0.3"};var J=navigator.userAgent.toLowerCase();if(J.indexOf("windows")>-1||J.indexOf("win32")>-1){Q.isWindows=true}else{if(J.indexOf("macintosh")>-1||J.indexOf("mac os x")>-1){Q.isMac=true}else{if(J.indexOf("linux")>-1){Q.isLinux=true}}}Q.isIE=J.indexOf("msie")>-1;Q.isIE6=J.indexOf("msie 6")>-1;Q.isIE7=J.indexOf("msie 7")>-1;Q.isGecko=J.indexOf("gecko")>-1&&J.indexOf("safari")==-1;Q.isWebKit=J.indexOf("applewebkit/")>-1;var ab=/#(.+)$/,af=/^(light|shadow)box\[(.*?)\]/i,az=/\s*([a-z_]*?)\s*=\s*(.+)\s*/,f=/[0-9a-z]+$/i,aD=/(.+\/)shadowbox\.js/i;var A=false,a=false,l={},z=0,R,ap;Q.current=-1;Q.dimensions=null;Q.ease=function(K){return 1+Math.pow(K-1,3)};Q.errorInfo={fla:{name:"Flash",url:"http://www.adobe.com/products/flashplayer/"},qt:{name:"QuickTime",url:"http://www.apple.com/quicktime/download/"},wmp:{name:"Windows Media Player",url:"http://www.microsoft.com/windows/windowsmedia/"},f4m:{name:"Flip4Mac",url:"http://www.flip4mac.com/wmv_download.htm"}};Q.gallery=[];Q.onReady=aj;Q.path=null;Q.player=null;Q.playerId="sb-player";Q.options={animate:true,animateFade:true,autoplayMovies:true,continuous:false,enableKeys:true,flashParams:{bgcolor:"#000000",allowfullscreen:true},flashVars:{},flashVersion:"9.0.115",handleOversize:"resize",handleUnsupported:"link",onChange:aj,onClose:aj,onFinish:aj,onOpen:aj,showMovieControls:true,skipSetup:false,slideshowDelay:0,viewportPadding:20};Q.getCurrent=function(){return Q.current>-1?Q.gallery[Q.current]:null};Q.hasNext=function(){return Q.gallery.length>1&&(Q.current!=Q.gallery.length-1||Q.options.continuous)};Q.isOpen=function(){return A};Q.isPaused=function(){return ap=="pause"};Q.applyOptions=function(K){l=aC({},Q.options);aC(Q.options,K)};Q.revertOptions=function(){aC(Q.options,l)};Q.init=function(aG,aJ){if(a){return}a=true;if(Q.skin.options){aC(Q.options,Q.skin.options)}if(aG){aC(Q.options,aG)}if(!Q.path){var aI,S=document.getElementsByTagName("script");for(var aH=0,K=S.length;aHaQ){aS=aQ-aM}var aG=2*aO+K;if(aJ+aG>aR){aJ=aR-aG}var S=(aN-aS)/aN,aP=(aH-aJ)/aH,aK=(S>0||aP>0);if(aL&&aK){if(S>aP){aJ=Math.round((aH/aN)*aS)}else{if(aP>S){aS=Math.round((aN/aH)*aJ)}}}Q.dimensions={height:aS+aI,width:aJ+K,innerHeight:aS,innerWidth:aJ,top:Math.floor((aQ-(aS+aM))/2+aO),left:Math.floor((aR-(aJ+aG))/2+aO),oversized:aK};return Q.dimensions};Q.makeGallery=function(aI){var K=[],aH=-1;if(typeof aI=="string"){aI=[aI]}if(typeof aI.length=="number"){aF(aI,function(aK,aL){if(aL.content){K[aK]=aL}else{K[aK]={content:aL}}});aH=0}else{if(aI.tagName){var S=Q.getCache(aI);aI=S?S:Q.makeObject(aI)}if(aI.gallery){K=[];var aJ;for(var aG in Q.cache){aJ=Q.cache[aG];if(aJ.gallery&&aJ.gallery==aI.gallery){if(aH==-1&&aJ.content==aI.content){aH=K.length}K.push(aJ)}}if(aH==-1){K.unshift(aI);aH=0}}else{K=[aI];aH=0}}aF(K,function(aK,aL){K[aK]=aC({},aL)});return[K,aH]};Q.makeObject=function(aH,aG){var aI={content:aH.href,title:aH.getAttribute("title")||"",link:aH};if(aG){aG=aC({},aG);aF(["player","title","height","width","gallery"],function(aJ,aK){if(typeof aG[aK]!="undefined"){aI[aK]=aG[aK];delete aG[aK]}});aI.options=aG}else{aI.options={}}if(!aI.player){aI.player=Q.getPlayer(aI.content)}var K=aH.getAttribute("rel");if(K){var S=K.match(af);if(S){aI.gallery=escape(S[2])}aF(K.split(";"),function(aJ,aK){S=aK.match(az);if(S){aI[S[1]]=S[2]}})}return aI};Q.getPlayer=function(aG){if(aG.indexOf("#")>-1&&aG.indexOf(document.location.href)==0){return"inline"}var aH=aG.indexOf("?");if(aH>-1){aG=aG.substring(0,aH)}var S,K=aG.match(f);if(K){S=K[0].toLowerCase()}if(S){if(Q.img&&Q.img.ext.indexOf(S)>-1){return"img"}if(Q.swf&&Q.swf.ext.indexOf(S)>-1){return"swf"}if(Q.flv&&Q.flv.ext.indexOf(S)>-1){return"flv"}if(Q.qt&&Q.qt.ext.indexOf(S)>-1){if(Q.wmp&&Q.wmp.ext.indexOf(S)>-1){return"qtwmp"}else{return"qt"}}if(Q.wmp&&Q.wmp.ext.indexOf(S)>-1){return"wmp"}}return"iframe"};function G(){var aH=Q.errorInfo,aI=Q.plugins,aK,aL,aO,aG,aN,S,aM,K;for(var aJ=0;aJ