Jump to content
  • Checkout
  • Login
  • Get in touch

osCommerce

The e-commerce.

UPS XML Rate Quote


Guest

Recommended Posts

Well, after realizing that the CGI/HTML version of qcostcgi.cgi is breaking down rapidly, I decided to whip up some quick XML Rate Quote service for the community to use. The qcostcgi.cgi program at UPS is what ALL OSCOMMERCE STORES RUN ON FOR UPS, and it's a DEPRECATED PROGRAM. My stores are getting around a 75% success rate on quotes, which is ridiculous. The amount of money we lost due to this is very, very large :)

 

I'm not a great programmer. I can hack out code very quickly though. The contribution below should be considered AN EMERGENCY REPLACEMENT ONLY. My hope is that Fritz (the author of UPS Choice) can take a look at this code and whip it into production quality code.

 

Either way I consider this a MAJOR ISSUE with OSCommerce. UPS can turn of the qcostcgi.cgi tomorrow (as they've indicated they will soon) and all OSCommerce stores relying on this antiquated rate method will be SCREWED.

 

All that being said, the below code is provided AS IS. It works on my Oct 02 2.2-CVS, and is based on UPS 1.47. It does NOT have the nice UPS Choice features. Hopefully someone will resolve that when they repackage this in a new module.

 

I have no desire to maintain, improve, or issue new versions of this source code. I've written major modules for OSCommerce in the past, and I simply do not have the time to make this module what you all want it to be. Someone else will have to pickup the ball on this, but the good news is that I've already done most of the architecture, now it just needs to be brought up for production use on servers with more error testing, and better UPS Choice style administration function.

 

Lastly, you'll need CURL support with your PHP installation for the XML parsing to work correctly.

 

All that being said, here is the code...

 

<?php
/*
  $Id: ups.php,v 1.47 2002/12/09 19:07:17 dgw_ Exp $
 
  osCommerce, Open Source E-Commerce Solutions
  http://www.oscommerce.com
 
  Copyright © 2002 osCommerce
 
  Released under the GNU General Public License
 
  MODIFIED:  Andrew Edmond <[email protected]> 
             June 18th, 2003
  CREDITS:   Modified from UPSTrack Class 
             by Jon Whitcraft <[email protected]> 
             Updated by Chris Lee (http://www.neox.net)
 
  This module is based on ups.php v1.47, NOT UPS Choice.  The author
  of UPS Choice for OSCommerce should be contacted to migrate changes
  to XML rate information into that program.  THERE IS NO SUPPORT
  PROVIDED FOR THIS UPGRADE / MODIFICATION AND IS PROVIDED "AS IS!".
 
  UPS Choice is currently only running CGI/HTML quotes and is 
  a service UPS might stop ANYTIME.  Write [email protected]
  to ask UPS Choice author to upgrade to XML rate quoting!
 
  NOTE: YOU HAVE TO EDIT the class "upsRate" below with your
        UPS Login/Password and Access Code.  Get your access code
        at: http://www.ec.ups.com/
 
*/
 
  class ups {
    var $code, $title, $descrption, $icon, $enabled, $types;
 
    function ups() {
      $this->code = 'ups';
      $this->title = MODULE_SHIPPING_UPS_TEXT_TITLE;
      $this->description = MODULE_SHIPPING_UPS_TEXT_DESCRIPTION;
      $this->icon = DIR_WS_ICONS . 'shipping_ups.gif';
      $this->enabled = MODULE_SHIPPING_UPS_STATUS;
 
      /*
       *  You can remove or comment out the type of packages that you do not wish to ship
       */
 
      $this->types = array (
                           '01' => 'UPS Next Day Air',
                           '02' => 'UPS 2nd Day Air',       
//                           '03' => 'UPS Ground',
//                           '07' => 'UPS Worldwide Express',
                           '08' => 'UPS Worldwide Expedited',
//                           '11' => 'UPS Standard',
//                           '12' => 'UPS 3 Day Select',
//                           '13' => 'UPS Next Day Air Saver',
//                           '14' => 'UPS Next Day Air Early A.M.',
//                           '54' => 'UPS Worldwide Express Plus',
                           '59' => 'UPS 2nd Day Air A.M.',
//                           '64' => '',
//                           '65' => 'UPS Express Saver',
                           );
 
    }
 
    function quote($method = '') {
      global $HTTP_POST_VARS, $order, $shipping_weight, $shipping_num_boxes, $cart;
 
      if ( (tep_not_null($method)) && (isset($this->types[$method])) ) {
        $prod = $method;
      } else {
        $prod = 'GND';
      }
 
      if ($method) $this->_upsAction('3'); // return a single quote
 
      $this->_upsProduct($prod);
 
      $country_name = tep_get_countries(STORE_COUNTRY, true);
      $this->_upsOrigin(STORE_ORIGIN_ZIP, $country_name['countries_iso_code_2']);
      $this->_upsDest($order->delivery['postcode'], $order->delivery['country']['iso_code_2']);
      $this->_upsRate(MODULE_SHIPPING_UPS_PICKUP);
      $this->_upsContainer(MODULE_SHIPPING_UPS_PACKAGE);
      $this->_upsWeight($shipping_weight);
      $this->_upsRescom(MODULE_SHIPPING_UPS_RES);
      $upsQuote = $this->_upsGetQuote();
 
      if (count($upsQuote)) 
      {
        $this->quotes = array('id' => $this->code,
                              'module' => $this->title . ' (' . $shipping_num_boxes . ' x ' . $shipping_weight . 'lbs)');
 
        $methods = array();
 
        if (($order->info['total'] > 50) && (($method == '')||($method == 'QFREEGND')))
        {
          $methods[] = array('id' => "QFREEGND",
                             'title' => "Free UPS Shipping",
                             'cost' => '0.00');
        }
        $qsize = count($upsQuote); 
        for ($i=0; $i<$qsize; $i++) 
        {
          $type = $upsQuote[$i]['service'];
          $cost= number_format($upsQuote[$i]['basic'],2);
 
          if ($this->types[$type] == "") continue;
          $methods[] = array('id' => $type,
                             'title' => $this->types[$type],
                             'cost' => (SHIPPING_HANDLING + $cost) * $shipping_num_boxes);
        }
        $this->quotes['methods'] = $methods;
      } 
      else 
      {
        $this->quotes = array('module' => $this->title,
                              'error' => 'An error occured with the UPS shipping calculations. ' .
                                          '<br>This is a problem with the UPS server.  Reload your browser ' .
                                          'to try the UPS server again.  If not, please call us ' . 
                                          'and we can manually process your order!');
     }
 
      if (tep_not_null($this->icon)) $this->quotes['icon'] = tep_image($this->icon, $this->title);
 
      return $this->quotes;
    }
 
    function check() {
      if (!isset($this->_check)) {
        $check_query = tep_db_query("select configuration_value from " . TABLE_CONFIGURATION . " where configuration_key = 'MODULE_SHIPPING_UPS_STATUS'");
        $this->_check = tep_db_num_rows($check_query);
      }
      return $this->_check;
    }
 
    function install() {
      tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('Enable UPS Shipping', 'MODULE_SHIPPING_UPS_STATUS', '1', 'Do you want to offer UPS shipping?', '6', '9', now())");
      tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('UPS Pickup Method', 'MODULE_SHIPPING_UPS_PICKUP', 'CC', 'How do you give packages to UPS? CC - Customer Counter, RDP - Daily Pickup, OTP - One Time Pickup, LC - Letter Center, OCA - On Call Air', '6', '10', now())");
      tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('UPS Packaging?', 'MODULE_SHIPPING_UPS_PACKAGE', 'CP', 'CP - Your Packaging, ULE - UPS Letter, UT - UPS Tube, UBE - UPS Express Box', '6', '11', now())");
      tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, date_added) values ('Residential Delivery?', 'MODULE_SHIPPING_UPS_RES', 'RES', 'Quote for Residential (RES) or Commercial Delivery (COM)', '6', '12', now())");
    }
 
    function remove() {
      tep_db_query("delete from " . TABLE_CONFIGURATION . " where configuration_key in ('" . implode("', '", $this->keys()) . "')");
    }
 
    function keys() {
      return array('MODULE_SHIPPING_UPS_STATUS', 'MODULE_SHIPPING_UPS_PICKUP', 'MODULE_SHIPPING_UPS_PACKAGE', 'MODULE_SHIPPING_UPS_RES');
    }
 
    function _upsProduct($prod){
      $this->_upsProductCode = $prod;
    }
 
    function _upsOrigin($postal, $country){
      $this->_upsOriginPostalCode = $postal;
      $this->_upsOriginCountryCode = $country;
    }
 
    function _upsDest($postal, $country){
      $postal = str_replace(' ', '', $postal);
 
      if ($country == 'US') {
        $this->_upsDestPostalCode = substr($postal, 0, 5);
      } else {
        $this->_upsDestPostalCode = $postal;
      }
 
      $this->_upsDestCountryCode = $country;
    }
 
    function _upsRate($foo) {
      switch ($foo) {
        case 'RDP':
          $this->_upsRateCode = 'Regular+Daily+Pickup';
          break;
        case 'OCA':
          $this->_upsRateCode = 'On+Call+Air';
          break;
        case 'OTP':
          $this->_upsRateCode = 'One+Time+Pickup';
          break;
        case 'LC':
          $this->_upsRateCode = 'Letter+Center';
          break;
        case 'CC':
          $this->_upsRateCode = 'Customer+Counter';
          break;
      }
    }
 
    function _upsContainer($foo) {
      switch ($foo) {
        case 'CP': // Customer Packaging
          $this->_upsContainerCode = '02';
          break;
        case 'ULE': // UPS Letter Envelope
          $this->_upsContainerCode = '01';
          break;
        case 'UT': // UPS Tube
          $this->_upsContainerCode = '03';
          break;
        case 'UEB': // UPS Express Box
          $this->_upsContainerCode = '21';
          break;
        case 'UW25': // UPS Worldwide 25 kilo
          $this->_upsContainerCode = '24';
          break;
        case 'UW10': // UPS Worldwide 10 kilo
          $this->_upsContainerCode = '25';
          break;
      }
    }
 
    function _upsWeight($foo) {
      $this->_upsPackageWeight = $foo;
    }
 
    function _upsRescom($foo) {
      switch ($foo) {
        case 'RES': // Residential Address
          $this->_upsResComCode = '1';
          break;
        case 'COM': // Commercial Address
          $this->_upsResComCode = '2';
          break;
      }
    }
 
    function _upsAction($action) {
      /* 3 - Single Quote
         4 - All Available Quotes */
 
      $this->_upsActionCode = $action;
    }
 
    function _upsGetQuote() 
    {
      if (!isset($this->_upsActionCode)) $this->_upsActionCode = '4';
 
      $upsXMLRate = new upsRate($this->_upsOriginPostalCode, '', $this->_upsOriginCountryCode);
      $upsXMLRateQuote = $upsXMLRate->rate('', $this->_upsDestPostalCode, '',$this->_upsDestCountryCode, $this->_upsPackageWeight, 1, $this->_upsContainerCode);
      return $upsXMLRateQuote;
    }
  }
 
class upsRate
{
   // You need to create userid ... at http://www.ec.ups.com
    var $userid = "your user id";
    var $passwd = "your password";
    var $accesskey = "your access code";
    var $upstool='https://www.ups.com/ups.app/xml/Rate';
    var $request;  //'rate' for single service or 'shop' for all possible services
    var $service;
    var $pickuptype='01'; // 01 daily pickup
      /* Pickup Type   
         01- Daily Pickup
         03- Customer Counter
         06- One Time Pickup
         07- On Call Air
         11- Suggested Retail Rates
         19- Letter Center
         20- Air Service Center
      */
    var $residential;
 
  //ship from location or shipper
    var $s_zip;
    var $s_state;
    var $s_country;
 
  //ship to location
    var $t_zip;
    var $t_state;
    var $t_country;
 
  //package info
    var $package_type;  // 02 customer supplied package
    var $weight;
    var $error=0;
    var $errormsg;
    var $xmlarray = array();   
    var $xmlreturndata = "";
 
    function upsRate($szip,$sstate,$scountry)
    {
       // init function
       $this->s_zip = $szip;
       $this->s_state = $sstate;
       $this->s_country = $scountry;
    }
 
    function construct_request_xml(){   
      $xml="<?xml version="1.0"?>
<AccessRequest xml:lang="en-US">
   <AccessLicenseNumber>$this->accesskey</AccessLicenseNumber>
   <UserId>$this->userid</UserId>
   <Password>$this->passwd</Password>
</AccessRequest>
<?xml version="1.0"?>
<RatingServiceSelectionRequest xml:lang="en-US">
  <Request>
    <TransactionReference>
      <CustomerContext>Rating and Service</CustomerContext>
      <XpciVersion>1.0001</XpciVersion>
    </TransactionReference>
    <RequestAction>Rate</RequestAction>
    <RequestOption>$this->request</RequestOption>
  </Request>   
  <PickupType>   
  <Code>$this->pickuptype</Code>
  </PickupType>
  <Shipment>   
    <Shipper>
      <Address>
    <StateProvinceCode>$this->s_state</StateProvinceCode>
    <PostalCode>$this->s_zip</PostalCode>
    <CountryCode>$this->s_country</CountryCode>
      </Address> 
    </Shipper>
    <ShipTo>
      <Address>
        <StateProvinceCode>$this->t_state</StateProvinceCode>
        <PostalCode>$this->t_zip</PostalCode>
        <CountryCode>$this->t_country</CountryCode>
        <ResidentialAddressIndicator>$this->residential</ResidentialAddressIndicator>
      </Address>
    </ShipTo>
    <Service>
    <Code>$this->service</Code>
    </Service>
    <Package>
      <PackagingType>
        <Code>$this->package_type</Code>
        <Description>Package</Description>
      </PackagingType>
      <Description>Rate Shopping</Description>
      <PackageWeight>  
        <Weight>$this->weight</Weight>
      </PackageWeight>
    </Package>
    <ShipmentServiceOptions/>
  </Shipment>
</RatingServiceSelectionRequest>";
       return $xml;
    }
 
    function rate($service='', $tzip, $tstate='', $tcountry='US', $weight, $residential=0, $packagetype='02')
    {
       if($service=='')
          $this->request = 'shop';
       else  
          $this->request = 'rate';
       $this->service = $service;
       $this->t_zip = $tzip;
       $this->t_state= $tstate;
       $this->t_country = $tcountry;
       $this->weight = $weight;
       $this->residential=$residential;
       $this->package_type=$packagetype;
 
       $this->__runCurl();
       $this->xmlarray = $this->_get_xml_array($this->xmlreturndata);
       //check if error occurred
       if($this->xmlarray=="")
       {
          $this->error=0;
          $this->errormsg="Unable to retrieve the Shipping info";
          return NULL;
       }
       $values = $this->xmlarray[RatingServiceSelectionResponse][Response][0];
       if($values[ResponseStatusCode] == 0)
       {
         $this->error=$values[Error][0][ErrorCode];
         $this->errormsg=$values[Error][0][ErrorDescription];
         return NULL;
       }
 
    return $this->get_rates();
    }
 
    function __runCurl()
    {
      $y = $this->construct_request_xml();
 
       $ch = curl_init();
       curl_setopt ($ch, CURLOPT_URL,"$this->upstool"); /// set the post-to url (do not includethe ?query+string here!)
       curl_setopt ($ch, CURLOPT_HEADER, 0); /// Header control
       curl_setopt ($ch, CURLOPT_POST, 1);  /// tell it to make a POST, not a GET
       curl_setopt ($ch, CURLOPT_POSTFIELDS, "$y");  /// put the querystring here startingwith "?"
       curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); /// This allows the output to be setinto a variable $xyz
       curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); /// some php version cause error withcurl without this line
       $this->xmlreturndata = curl_exec ($ch); /// execute the curl session and return theoutput to a variable $xyz
 
       curl_close ($ch); /// close the curl session
    }
 
    function __get_xml_array($values, &$i)
    {
      $child = array();
      if ($values[$i]['value']) array_push($child, $values[$i]['value']);
 
      while (++$i < count($values))
      {
        switch ($values[$i]['type'])
        {
          case 'cdata':
            array_push($child, $values[$i]['value']);
          break;
 
          case 'complete':
            $name = $values[$i]['tag'];
            $child[$name]= $values[$i]['value'];
            if($values[$i]['attributes'])
            {
              $child[$name] = $values[$i]['attributes'];
            }
          break;
 
          case 'open':
            $name = $values[$i]['tag'];
            $size = sizeof($child[$name]);
            if($values[$i]['attributes'])
            {
                 $child[$name][$size] = $values[$i]['attributes'];
                  $child[$name][$size] = $this->__get_xml_array($values, $i);
            }
            else
            {
                  $child[$name][$size] = $this->__get_xml_array($values, $i);
            }
          break;
          case 'close':
            return $child;
          break;
        }
      }
      return $child;
    }
 
    function _get_xml_array($data)
    {
      $values = array();
      $index = array();
      $array = array();
      $parser = xml_parser_create();
      xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
      xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
      xml_parse_into_struct($parser, $data, $values, $index);
      xml_parser_free($parser);
 
      $i = 0;
 
      $name = $values[$i]['tag'];
      $array[$name] = $values[$i]['attributes'];
      $array[$name] = $this->__get_xml_array($values, $i);
 
      return $array;
    }
    function get_rates()
    {
     // $retArray = array('service'=>'','basic'=>0,'option'=>0,'total'=>0,'days'=>'','time'=>'');
 
        $retArray=array();
 
        $values = $this->xmlarray[RatingServiceSelectionResponse][RatedShipment];
      $ct = count($values);
        for($i=0;$i<$ct;$i++)
        {
          $current=&$values[$i];
          $retArray[$i]['service'] = $current[service][0][code];
          $retArray[$i]['basic'] = $current[TransportationCharges][0][MonetaryValue];
          $retArray[$i]['option'] = $current[serviceOptionsCharges][0][MonetaryValue];
          $retArray[$i]['total'] = $current[TotalCharges][0][MonetaryValue];
          $retArray[$i]['days'] =  $current[GuaranteedDaysToDelivery];
          $retArray[$i]['time'] = $current[scheduledDeliveryTime];
        }
        unset($values);
 
        return $retArray;
    }
}
?>

Link to comment
Share on other sites

CURL is, if I'm correct, a system based program. You need to add it to the server. Another popular program that's pretty similar is wget.

 

Both these programs are command-line unix/bsd ftp like programs.

NewsDesk(934) / FAQDesk(1106) / OrderCheck(1168) :::

Link to comment
Share on other sites

Can't be any worse than the current UPS module...

 

I was in the process of downloading the UPS Shipping XML Dev kit when I saw this post...

 

Many thanks, it gives me a nice place to start :D

The only thing necessary for evil to flourish is for good men to do nothing

- Edmund Burke

Link to comment
Share on other sites

There are two versions of cURL support - command line and script (libcurl). They need to allow for script support.

 

It's a pretty straight forward error - it's basically telling you that the webserver doesn't understand the curl_init() function which tells me the host doesn't support cURL.

Apathy is a dominant gene - mutate.

Link to comment
Share on other sites

Nothing happened when I installed the xml ups module. The UPS heading was listed but no rate quotes or errors. Is there something I can set to help identify if I communicated with UPS OK and view the response? Any other way to debug? I'm really excited to have this xml module to work with but I'm not sure what to do next.

Millie

Link to comment
Share on other sites

Lorax,

 

My ISP recompiled their PHP and now I am no longer getting the error. I'm getting a whole new error :P

 

I get quotes from USPS, but UPS says

 

An error occured with the UPS shipping calculations.

This is a problem with the UPS server. Reload your browser to try the UPS server again. If not, please call us and we can manually process your order!

 

I do have an ID/PWD and those are configured.

 

 

D.

Link to comment
Share on other sites

It may be a timeout issue. Try building a loop around the code that queries the UPS server (loop for some number like 6-10). Use a false return to loop again and a true to break out of the loop.

Apathy is a dominant gene - mutate.

Link to comment
Share on other sites

Check out the contributions section for "UPS XML Rates and Services v1.0". I only posted it five minutes ago. I'm not sure if the moderators have to rubber stamp it before it becomes available.

 

Please, please tell me if you encounter any errors, omissions, or if it isn't completely wonderful. I want to update it before it becomes really popular.

 

 

Torin...

Link to comment
Share on other sites

First of all, Thank you, Torin! I really appreciate the work you have done to get this working. I was able to install in a few minutes and obtain rates!

 

The rate returned was several dollars too high and I could see that I needed to change the weight to LBS. I found 2 places in /includes/modules/shipping/upsxml.php that needed to be changed but the shipping quote did not go down to the expected amount (did not change at all). I wanted to suggest documenting the specific changes. I think it will help all who need to change from KG to LBS.

Thanks!

Millie

Link to comment
Share on other sites

You're right, I had a bit of difficulty with the LB vs KG when using a Canadian Origin. It didn't accept KG, and figured it had something to do with the Origin. I'll work on it a bit more and publish a patch for both the code, if necessary, and the documentation.

 

Give me a day or so. It's the weekend, and I'm installing new countertops in the kitchen.

 

 

Torin...

Link to comment
Share on other sites

Hi Torin,

I've been playing with the settings and can not get the rates to return close to actual costs. Looking at the log, I think the weight is being processed differently than in the old ups.php (where is my packaging weight that should be added on?) and a handling fee does not add on.

 

I process weights differently than most, I use $total_weight*.0625 in shipping.php to convert ounces to pounds. In upsxml.php, the shipping weight is shown accurately in pounds when viewing the rate chart from the web but in the upsxml log it shows ounces without the packaging weight included. When I added code to upsxml.php to convert ounces to pounds the log showed the correct weight (weight of packaging still missing) but the rates remained incorrect.

 

The amount of error in the rate calculation is several dollars more than what a proper weight would correct so I think something else is going on as well...

 

I'm not skilled at php or xml so my observations may not be professional but I hope sharing them will help identify bugs in the module.

Millie

Link to comment
Share on other sites

Guys and Gals,

 

Upon closer inspection, I realized I copied some minor bugs from another module. Naturally, there were a few of my own. Incorrect rates and inflexible units of measurements are two of the symptoms. I will publish a corrected release very shortly.

 

I have been trying very hard to produce a packaging feature whereby one can configure a number of boxes (length, width, height, max weight) for use by the shipping module. The shipping module will use a simple algorithm to pack products into as few boxes as possible, and return rates on the boxes, rather than the number of products.

 

For example, if one sells CDs which have a nominal dimension of 5x5x0.5 inches, one would configure a few boxes: A single CD mailer of 5x5x0.5 inches, a five-CD mailer of 5x5x2.5 inches, and a ten-CD mailer with dimensions of 5x5x5 inches.

 

The algoritm will attempt to ship the CDs in as few packages as possible and quote on the rates for those packages. The hope is that this algoritm will be smarter than just quoting on the total number of products and save you, the storeowner, some money.

 

This featureset requires a few easy modifications and has no impact on other services. The only work I have left is to make dimension conversion work (we Canadians face the dillemma of working with and developing for both Metric and Imperial Systems) and clean up the pop-up text on buttons.

 

It would be extremely helpful if you could tell me whether a feature like this is desirable. It will change the way I release this in the future. Right now, I'm practically splitting the module in half. One set of instructions for those who want to simply plug in the module, and another set for those who opt for the dimension support. If I learn that most people will want the dimension support, I will remove the dividers.

 

 

Torin...

Link to comment
Share on other sites

Just my opinion. I think you should do it both ways. Dimension and no dimension. Some of my products are very big and require oversized boxes so the dimension would not help me at all.

 

Now if you could have this option product specific that would be a plus. One product uses the dimension options and other products do not.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...