Proxy & Debug SOAP Requests in PHP

crm_soap_php1Last week, we needed to debug soap requests in php due an issue on our live environment. We attempted to integrate our Magento store with an external provider that used SOAP to communicate to our severs. Everything was working fine on our testing/staging environments, but when we went live, they contacted us due an issue in the connection:

Error Fetching http body, No Content-Length, connection closed or chunked data.

After a quick search, we found an easy fix by falling back to HTTP 1.0 on the client connection:

$client = new SoapClient("http://yourserver.com/api/?wsdl",

array(
 'stream_context' => stream_context_create(array('http' => array('protocol_version' => 1.0) ) )
 )
 );

However, to make things more interesting, the external service wasn’t willing to do any changes on their side, which means that we needed to find another solution, on our side.

It did seem to be an environment related issue rather than a code problem. Anyway, we tried to debug the whole connection flow, to see if we could find clues of how to solve the problem.

We have a sample script that we use to debug connections locally (using xdebug), with the code below:


class Debug_SoapClient extends SoapClient
{
 public function __doRequest($request, $location, $action, $version, $one_way = 0)
 {
 $aHeaders = array(
 'Method: POST',
 'Connection: Close',
 'Content-Type: text/xml',
 'SOAPAction: "'.$action.'"'
 );

$ch = curl_init($location);
 curl_setopt_array(
 $ch,
 array(
 CURLOPT_VERBOSE => false,
 CURLOPT_RETURNTRANSFER => true,
 CURLOPT_POST => true,
 CURLOPT_POSTFIELDS => $request,
 CURLOPT_HEADER => false,
 CURLOPT_HTTPHEADER => $aHeaders,
 CURLOPT_SSL_VERIFYPEER => true,
 CURLOPT_COOKIE => "XDEBUG_SESSION=PHPSTORM"
 )
 );

$ret = curl_exec($ch);
 return $ret;
 }
}

//Use soap as always, just replacing the classname while instantiating the object:
$client = new Debug_SoapClient('http://myurl.com/api/?wsdl');

However, we can’t use xdebug on live environment, so we tried to find a way to debug the connection flow, so that we could compare the differences of using HTTP 1.0  vs the standard way.

After a couple of unsuccessful attempts, we found a hacky way to proxy-forward the requests through curl, so that we could debug the input and the output of each call.

Firstly, we created a test php file that would log the environment variables on a file for each request, and then it would forward it to the soap api:

file_put_contents(__DIR__ . '/soap.log', var_export($_SERVER, true), FILE_APPEND);
file_put_contents(__DIR__ . '/soap.log', var_export($_REQUEST, true), FILE_APPEND);
file_put_contents(__DIR__ . '/soap.log', var_export($_GET, true), FILE_APPEND);
file_put_contents(__DIR__ . '/soap.log', var_export($_POST, true), FILE_APPEND);
file_put_contents(__DIR__ . '/soap.log', var_export($HTTP_RAW_POST_DATA, true), FILE_APPEND);
file_put_contents(__DIR__ . '/soap.log', var_export($_GET, true), FILE_APPEND);
// Override the REQUEST_URI variable so that the framework can understand and process the soap request
$_SERVER['REQUEST_URI'] = '/api/?wsdl';
include 'index.php';

Note that sometimes $_POST might be empty but $HTTP_RAW_POST_DATA might contain data.

After comparing the logs of the two requests, we noticed few differences on the $_SERVER variables, but even overriding the values didn’t help.

More research revealed that the issue might be related with a bug in some php versions:

It is an HTTP 1.1 issue with some versions of PHP not properly decoding chunked data (even some versions of PHP 5.3.x will still see this error, the documentation on PHP’s official site is wrong). You have two options in this case:

(1) Update your version of PHP to 5.4.x or later.
(2) Force the client library to use HTTP 1.0

We tried to connect using another app-server with an older php version, and it worked without having to fallback to http 1.0.

Then we thought about proxy-ing the requests to the other server internally, just to test if that would solve the issue. Eventually we managed to get the requests forwarded with another script file:


header('Content-Type: text/xml; charset=UTF-8');
$ch = curl_init('https://external.server.com/soaptest.php/' . ($_GET ? '?' . http_build_query($_GET) : 'api/index/index/'));
curl_setopt_array(
 $ch,
 array(
 CURLOPT_VERBOSE => false,
 CURLOPT_RETURNTRANSFER => true,
 CURLOPT_POST => $_SERVER['REQUEST_METHOD'] == 'POST' ? true : false,
 CURLOPT_POSTFIELDS => $HTTP_RAW_POST_DATA,
 CURLOPT_HEADER => false,
 CURLOPT_SSL_VERIFYPEER => false,
 CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0,
 )
 );
$ret = curl_exec($ch);
file_put_contents(__DIR__ . '/soap.log', var_export($ret, true), FILE_APPEND);
echo $ret;

With this script curl-ing the previous one on the other server, we managed to get the SOAP calls to work. It’s something that we did only for debugging purposes, but hopefully it can be useful for you in case you face similar issues and you want to discard possibilities. Sadly, that still didn’t solve the issue for the non http 1.0 requests, but at least we were able to compare all the inputs and all the outputs and we noticed that it was all working fine both ways until the actual soap call:

– Instantiating the class with the url

– Authenticating to soap by calling login()

– Calling to a custom soap call: ie. sales_order.info would return the proper results in both cases, but only the http 1.0 connection would properly retrieve them. The standard connection would retry the call and eventually display the error message showed below.

Eventually, we solved the issue by temporarily making the requests directly hitting the old php version server, until the php version was upgraded on the non working server.

How to prevent a AlertDialog from closing when a button is clicked

prevent a AlertDialog from closing when a button is clicked
You can prevent a AlertDialog from closing when a button is clicked.

It’s very easy you just need to use the following code in your application:

final AlertDialog dialog = new AlertDialog.Builder(context)
        .setView(v)
        .setTitle(R.string.my_title)
        .setPositiveButton(android.R.string.ok, null) //Set to null. We override the onclick
        .setNegativeButton(android.R.string.cancel, null)
        .create();

dialog.setOnShowListener(new DialogInterface.OnShowListener() {

    @Override
    public void onShow(DialogInterface dialog) {

        Button b = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
        b.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                // TODO Do something

            }
        });
    }
});

Custom theme

In this post you create your custom theme.

Custom theme

The first step is generate the drawable.

The second step import AppCompat library.

The third step in res/values-v14/style.xml

<!-- general styles for the action bar -->

<style>
<item name="android:popupMenuStyle">@style/PopupMenu.Example</item>
        <item name="android:dropDownListViewStyle">@style/DropDownListView.Example</item>
        <item name="android:actionBarStyle">@style/MyTheme.ActionBarStyle</item>
        <item name="android:actionBarTabStyle">@style/ActionBarTabStyle.Example</item>
        <item name="android:actionDropDownStyle">@style/DropDownNav.Example</item>
        <item name="android:actionBarStyle">@style/ActionBar.Solid.Example</item>
        <item name="android:actionModeBackground">@drawable/cab_background_top_example</item>
        <item name="android:actionModeSplitBackground">@drawable/cab_background_bottom_example</item>

</style>
<style>
<item name="android:background">@drawable/ab_solid_example</item>
        <item name="android:backgroundStacked">@drawable/ab_stacked_solid_example</item>
        <item name="android:backgroundSplit">@drawable/ab_bottom_solid_example</item>
        <item name="android:textColor">@color/blue</item>
        <item name="android:titleTextStyle">@style/MyTheme.ActionBar.TitleTextStyle</item>
</style>

<style>
<item name="android:textColor">@color/white</item>
<item name="android:textStyle">bold</item>

</style>

<style>
<item name="android:background">@drawable/ab_transparent_example</item>
<item name="android:progressBarStyle">@style/ProgressBar.Example</item>
</style>

<style>
<item name="android:popupBackground">@drawable/menu_dropdown_panel_example</item>
</style>

<style>
<item name="android:listSelector">@drawable/menu_dropdown_panel_example</item>
</style>

<style>
<item name="android:background">@drawable/tab_selected_example</item>
</style>

<style>
<item name="android:background">@drawable/spinner_ab_default_example</item>
<item name="android:popupBackground">@drawable/menu_dropdown_panel_example</item>
<item name="android:dropDownSelector">@drawable/menu_dropdown_panel_example</item>
</style>

<!-- this style is only referenced in a Light.DarkActionBar based theme -->

<style>
<item name="android:popupMenuStyle">@style/PopupMenu.Example</item>
<item name="android:dropDownListViewStyle">@style/DropDownListView.Example</item>
</style>

Vertical and horizontal scrolling

Sometimes you need to show a lot of information in a small space,

for example in TableLayout, you can to make a vertical and horizontal scrolling.

 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="fill_parent"
                  android:layout_height="wrap_content">

    <HorizontalScrollView 
                  android:layout_width="wrap_content"
                  android:layout_height="fill_parent">

         <TableLayout
                  android:id="@+id/amortization"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content">

              <TableRow
                  android:background="#3366FF">
                  <TextView
                       android:text="@string/tableRow_1"
                       android:background="#FFFFFF"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_1"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_2"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_3"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_4"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_5"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_6"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_7"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_8"
                       android:padding="3dip"/>
                  <TextView
                       android:text="@string/hello_world_9"
                       android:padding="3dip"/>
              </TableRow>
         </TableLayout>
    </HorizontalScrollView>
</ScrollView>

you can download this code: download code

Integrating with Twitter on Android

Integrating with Twitter on Android

The first step:

To create a button in your .xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".TheDeveloperWorldIsYours" >

    <button
        android:id="@+id/twitterButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>

Now in your .class, you should your dialog, this form.

public class TheDeveloperWorldIsYours extends Activity {
	private Button mTwitterButton;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_the_developer_world_is_yours);

		mTwitterButton = (Button) findViewById(R.id.twitterButton);
		mTwitterButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {

				// TODO Auto-generated method stub

				Intent share = findTwitterClient();

				if (share != null) {

					share.setType("text/plain");

					share.putExtra(Intent.EXTRA_TEXT, "TEXT");

					startActivity(share);
				}

				else {

					AlertDialog.Builder builder = new AlertDialog.Builder(
							TheDeveloperWorldIsYours.this);

					builder.setMessage("Instal twitter")

					.setTitle("¡attention!")

					.setCancelable(false)

					.setNeutralButton("Accept",

					new DialogInterface.OnClickListener() {

						public void onClick(DialogInterface dialog, int id) {

							dialog.cancel();

						}

					});

					AlertDialog alert = builder.create();

					alert.show();

					System.out.println("TEXT");

				}

			}

		});
	}

	public Intent findTwitterClient() {
	    final String[] twitterApps = {
	            // package // name - nb installs (thousands)
	            "com.twitter.android", // official - 10 000
	            "com.twidroid", // twidroid - 5 000
	            "com.handmark.tweetcaster", // Tweecaster - 5 000
	            "com.thedeck.android" }; // TweetDeck - 5 000 };
	    Intent tweetIntent = new Intent();
	    tweetIntent.setType("text/plain");
	    final PackageManager packageManager = getPackageManager();
	    List list = packageManager.queryIntentActivities(
	            tweetIntent, PackageManager.MATCH_DEFAULT_ONLY);

	    for (int i = 0; i < twitterApps.length; i++) {
	        for (ResolveInfo resolveInfo : list) {
	            String p = resolveInfo.activityInfo.packageName;
	            if (p != null && p.startsWith(twitterApps[i])) {
	                tweetIntent.setPackage(p);
	                return tweetIntent;
	            }
	        }
	    }
	    return null;

	}

}