Sunday, September 25, 2016

Sencha Touch Hide Alert Box on Android Back Button Press

Hello,

Recently in one my project our client gave use very strange requirement. We have used Sencha Touch to create application and used Cordova to create native app.

As we all know with Sencha Touch we use Ext.Msg.alert() to show user alert. This alert has OK button. When user tap on that alert goes away.

However in our case client asked us to hide this alert if its on and user presses virtual or physical hardware button of android phone. So after hearing this requirement first of I was confused and was not sure how to implement it. But after looking at docs and source code of Ext.Msg class solution was very easy so on this blog I am going to explain how to do this.

First of all we have to bind back button key event. Add following code to your app.js file.

if (Ext.os.is('Android')) {
            document.addEventListener("backbutton", Ext.bind(onBackKeyDown, this), false);
            function onBackKeyDown(e) {
            }
}

Now as we all know Ext.Msg is singleton class and the alerts and confirm boxes are nothing but a floating panels. So we can just simply check if it's hidden or not and hide it if required. Check the following code.


if (Ext.os.is('Android')) {
            document.addEventListener("backbutton", Ext.bind(onBackKeyDown, this), false);
            function onBackKeyDown(e) {
                     if(Ext.Msg.isHidden() !=  null){
                             if(Ext.Msg.isHidden() == false){
                                       Ext.Msg.hide();
                             }
                     }
                     else{
                             var comp = Ext.getCmp("ext-sheet-1");
                             if(comp){
                                 comp.hide();
                            }
                     }
                     e.preventDefault();
            }
}


As you can see in above code. First we are checking Ext.Msg.isHidden() !=  null, this is to check if there is no instance of alert is created yet, there is no meaning of hiding it.

Then we check if Ext.Msg.isHidden() == false then just hide it or else don't do anything. That's it. Hope this helps you.

Cordova Applicaiton Select Any File From SD Card and Upload to Server

Hello,

Recently in one of my project we were building android application with Cordova. There was a requirement where user can choose any files from SD card and upload it to server.

So in this blog I am going to explain how to do this.

Our challenges was user can pick any file like audio, video, image, pdf etc. So we have to properly save it on server with proper extension.

First of all you will need two plugins.

1) Cordova File Transfer Plugin

You can install it via following command.

cordova plugin add cordova-plugin-file-transfer

2) File Chooser Plugin

You can check this plugin here and can install it via following command.

cordova plugin add http://github.com/don/cordova-filechooser.git

Now once you install file choose plugin you can use following command to open file selector in android.

fileChooser.open(function(uri) {
    alert(uri);
});

Now the problem with this file chooser plugin is that it gives content path like this.

content://media/images/4

However this path can not be used with File Transfer Plugin as it needs absolute file URI like this.

file:///sdcard/0/downloads/myPDF.pdf

So we have to edit this file chooser plugin little bit. Go to your android project and open FileChooser.java file from the com.megster.cordova package and check the following function.


public void onActivityResult(int requestCode, int resultCode, Intent data) {

}

See the following line of code in this function.

Uri uri = data.getData();

 callback.success(uri.getPath());

So form here it returns the content path.

We have to convert that content path to absolute path. So add following functions in FileChooser.java


public static String getPath(final Context context, final Uri uri) {

        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }

                // TODO handle non-primary volumes
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[] {
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @param selection (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
            String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }


    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());

    }

Now update onActivityResult function as follow.

callback.success(getPath(cordova.getActivity(), uri));

It will send you correct absolute URI.  Now in your JavaScript.

var filePath = '';
var fileType = '';

fileChooser.open(function(obj) {
            filePath = 'file://'+obj;
            fileType= obj.substring(uri.lastIndexOf('.'));
        });

As you see above we are storing file path and file extension in two variables. Now following is the code to upload your file to server.

        var win = function (r) {
                alert('file uploaded');
        }

        var fail = function (error) {
         }

        var options = new FileUploadOptions();
        options.fileKey = "file";
        options.fileName = filePath.substr(this.evidencePath.lastIndexOf('/') + 1);
        options.mimeType = "text/plain";

        var params = {};
        params.fileType = fileType;
        options.params = params;

        var ft = new FileTransfer();
        ft.upload(filePath, encodeURI("http://yourserverpath"), win, fail, options);

On the server side your PHP code should be as follow.

$file_type = $_POST['fileType'];
$fileName = time()."_".$file_type;
move_uploaded_file($_FILES["file"]["tmp_name"], "/yourserverpath/".$fileName);
return json_encode(array('success'=>true,'index'=>$index, 'server_path'=>"http://serverpath".$fileName));

That's it, hope this helps you.

Saturday, September 17, 2016

osTicket, Create Ticket with API

Hello,

Recently for one of my project we have a requirement to integrate laravel application and osTicket. Data from laravel app to be sent to osTicket and create ticket with that.

Now we all know that in osTicket if send an email, it will automatically create ticket. However in our case it won't work as emails were from different domains so we have to use osTicket API.

First of all you have to create API key for that login to your osTicket admin panel and go to.

Admin Panel -> Manage -> API keys and create new API key.


Here IP address is your source IP address from where you will call an API. In our case it was server IP address. Now API key is created. Following is the code.

$config = array(
'url'=>'http://yourosTicketURL/api/tickets.json', 
'key'=>'yourapikey'  // API Key goes here
);


In above code replace yourosTicketURL with your URL and replace yourapikey with your generated key.

Now prepare data to send.

$name = 'Name of User';
$email = 'Email of User';
$mobile = 'Mobile';
$subject = 'Subject';
$text = 'Dummy message';

$data = array(
'name'      =>     $name,
'email'     =>      $email,
'phone' =>     $mobile,
'subject'   =>      $subject,
'message'   =>   "data:text/html;charset=utf-8,$text",
'ip'        =>      $_SERVER['REMOTE_ADDR'],
'topicId'   =>      '1',
'attachments' => array()
);

Now we will send data to API.

#pre-checks
function_exists('curl_version') or die('CURL support required');
function_exists('json_encode') or die('JSON support required');

#set timeout
set_time_limit(30);

#curl post
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $config['url']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_USERAGENT, 'osTicket API Client v1.333');
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Expect:', 'X_API_KEY: '.$config['key']));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$result=curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($code != 201)
die('Unable to create ticket: '.$result);

$ticket_id = (int) $result;

# Continue onward here if necessary. $ticket_id has the ID number of the
# newly-created ticket

function IsNullOrEmptyString($question){
return (!isset($question) || trim($question)==='');
}

That's it and it should work. No wait if you run you will always get following error.

Unable to create ticket:  Invalid API key.

Now that was really strange as API key was valid, IP address was valid everything is good but still it does not create ticket. So what to do. Well there is a bug in osTicket API code. After spending couple of hours I finally figure out the solution. We have to modify osTicket source code for this. Open following file.

include/class.api.php

And find out following functions.

function requireApiKey() {
        # Validate the API key -- required to be sent via the X-API-Key
        # header

        if(!($key=$this->getApiKey()))
            return $this->exerr(401, __('Valid API key required'));
        elseif (!$key->isActive() || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR'])
            return $this->exerr(401, __('API key not found/active or source IP not authorized'));

        return $key;
    }

    function getApiKey() {

        if (!$this->apikey && isset($_SERVER['HTTP_X_API_KEY']) && isset($_SERVER['REMOTE_ADDR']))
            $this->apikey = API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR']);

        return $this->apikey;
    }

Here the problem was it never get HTTP_X_API_KEY and REMOTE_ADDR was never set. So replace above two function with following code.

function requireApiKey() {
        # Validate the API key -- required to be sent via the X-API-Key
        # header
        if(!($key=$this->getApiKey()))
            return $this->exerr(401, __('Valid API key required'));
        elseif (!$key->isActive() || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR'])
            return $this->exerr(401, __('API key not found/active or source IP not authorized'));

        return $key;
    }

    function getApiKey() {
 
  foreach (getallheaders() as $name => $value) {
  $_SERVER['HTTP_'.$name] = $value;
  }
    if (!$this->apikey && isset($_SERVER['HTTP_X-Api-Key']) && isset($_SERVER['REMOTE_ADDR']))
            $this->apikey = API::lookupByKey($_SERVER['HTTP_X-Api-Key'], $_SERVER['REMOTE_ADDR']);

        return $this->apikey;
    }

As you can see in above code we are getting all headers we set when we call API and put it in $_SERVER variable. and instead of REMOTE_ADDR we are using HTTP_X_REAL_IP.  Now save the file and again run API code. It should work fine.




Saturday, September 10, 2016

Laravel 5.3 Run Artisan Command Through Routes

Recently for one my project we installed Laravel 5.3 on shared server where we were not having SSH or route access.

As we know general practice with Laravel is to create database tables with migrations and for that we have to run artisan commands through terminal and SSH.

But in our case we were not having SSH access, we have only FTP and database access so we have to figure out a way to run this commands through web. In this blog I am going to explain how to do this.

After reading Laravel 5.3 document I came to know that Laravel gives you Artisan class to run artisan command. So what I did is I created two routes, one for creating migration and other to run migration.

To create migration

Route::get('/create-migration-command', function () {
    Artisan::call('make:migration', ['name' => 'migration_name']);
});

As you can see here the command is make:migration and parameter passed is name of migration. 

Virtually it is equivalent to following SSH command.

php artisan make:migration migration_name

This will generate migration file in database folder, you can open it through FTP and add necessary code of migration.

Run Migration

Route::get('/run-migration-command', function () {
    Artisan::call('migrate', []);
});

Virtually it is equivalent to following SSH command.

php artisan migrate

Hope this helps you.

Laravel 5.3 Internal Server Error After Installation

Recently for one of my project I have installed Laravel 5.3 on server but after installation when I tried to run it in web it was showing 500 Internal Server Error so I had to spend few hours in resolving it so here in this blog I am going to explain what was the issue and how I solved it.

1) Find out the Error.

I looked into to server logs from cPanel and found out following error.

SoftException in Application.cpp:429: Mismatch between target UID (501) and UID (99) of file "/path/larravel/public/index.php"


After searching for sometime about this error I figured out that the apache or web user was not owner for the files because I created laravel project through SSH so the logged in SSH user was the owner of the files and folders.

So when apache or web user tried to access the files it was not working. So the solution was to change the ownership of files and folders to apache user.

2) Find out the username of apache user.

In most cases user name of apache user should be www-data or apache or apache2 but in my case since I was using VPS the username was different. So run following command in terminal through SSH

ls -l

It will give you file list with user and group owner. That's your web username. Now change the ownership of folder to this username.


chown -R username:username laravel

That's it. Now run your laravel project and it shall display following screen.

That means your laravel project is working fine on the server. Hope this helps you.

Saturday, September 3, 2016

PDF.JS Not Working - Sencha Touch 2 PDF Panel Not Working After Build

Hello,

Recently in one of my project we were using following Sencha Touch 2 PDF panel to display PDF in native application build with Cordova.

I faced a strange issue here. App was working fine without any build but when I generate a production build and build the native android app PDF panel stopped working and throws following error.

Unhandled rejection: TypeError: undefined is not an object (evaluating 'viewBox')

I tried to find out a solution for sometime but could not get any. I was using following PDF panel.

https://github.com/SunboX/st2_pdf_panel

Suddenly I saw that there is no update since last three years and that could be probably be the reason. So what I did is downloaded latest PDF.js and built the minified files and used it and bingo it worked. So here I am going to explain the steps.

1) First of clone latest PDF.js source from it's git.

$ git clone git://github.com/mozilla/pdf.js.git

2) Now go to the folder.

$ cd pdf.js

3) Install Node.js if its not installed.

$ npm install -g gulp-cli

4) After node.js is installed, install all the dependencies.

$ npm install

5) Build PDF.js files.

$ gulp generic

it will generate minified files in directory build/generic/build/

Copy pdf.js and pdf.worker.js files from this directory to your lib folder and that's it now PDF will be displayed even in testing or production build.