Sunday, December 29, 2013

Yii Framework: Using multiple db connections in a controller method

I have a mixed database where the majority of the tables are UTF-8 encoding but there are a few tables that are required to store the data in a specific encoding that this not UTF-8. 

In Yii you are defining you global database connection in the protected/config/main.php file, and this is global for the entire web application.This is causing some challenges for me since it is very important that I deliver data bitwise compatible according to the data stored in the table. 

I was not able to find any solution to this in my first few Google attempts, so I had to dive into the code.

First I tried in the controller action method by changing the charset on the global CDbConnection directly using 

Yii::app()->db->charset='latin1';

this did not work since the database connection is already initialized at this point.

Going through the Yii Framework source code I first found CApplication->getDB(), then searching for the function getComponent() lead me to CModule->getComponent().

My next though was "If there is a getComponent(), I wonder if there is not a setComponent() function as well?" - and behold CModule->setComponent().

Armed with this knowledge it was easy to change the connection temporarily to use a new initialized database connection with a different charset.

public function loadModel($id)
{
    // Save the original database connection for later reuse
    $originalDbConnection = Yii::app()->db;
    
    // Create a new database connection based on the original database connection
    // change the charset to latin1
    $latin1DbConnection = Yii::createComponent(array(      
        'class'=>'CDbConnection',
        'connectionString' => Yii::app()->db->connectionString,
        'emulatePrepare' => Yii::app()->db->emulatePrepare,
        'username' => Yii::app()->db->username,
        'password' => Yii::app()->db->password,
        'charset' => 'latin1',
    ));

    // Set the application wide database connection for this Apache/PHP webrequest to use this special database connection
    Yii::app()->setComponent("db",$latin1DbConnection);
    
    // Do the queries against the database
    $someModel=SomeModel::model();
    $model = $someModel->findByPk($id);
    
    // Restore the original database connection 
    // note: 
    // This is actually not needed in this simple case, due to the fact that Apache/PHP rebuilds the entire web application on each request
    // Since I am not doing additional request in this logic here after returning the result everything is torn down and discarded.
    // However I have added it as part of this code for completeness and also in case someone else out there is doing more complex logic with multiple 
    // request that needs different charsets
    Yii::app()->setComponent("db",$originalDbConnection);

    if($model===null)
        throw new CHttpException(404,'The requested page does not exist.');
    return $model;
}

Sunday, September 01, 2013

Unable to change network adapter UUID on Linux

I needed to reconfigure the hardware UUID of a Linux network adapter caused by cloning a virtual machine. To do this you need to either remove the /etc/udev/rules.d/70-persistent-net.rules and let the system regenerate the file on reboot, or manually edit the file.

I decided to edit the file and manually correct the UUID. I was consistently met "readonly file" so I could not save my changes.

I found using the lsattr /etc/udev/rules.d/70-persistent-net.rules that the file had the i attribute set. This i attribute means that the file is immutable. Removing this attribute using chattr -i /etc/udev/rules.d/70-persistent-net.rules, allowed me to change the file as needed. After editing the file I set the i attribute again using chattr +i /etc/udev/rules.d/70-persistent-net.rules.

Sunday, July 28, 2013

Fixing the client IP as perceived by Apache behind Nginx reverse proxy

In my recent post Using Nginx to reverse proxy a secure apache site that is using socket.io/node.js/websockets I found that I was only getting the proxy's IP address in all of my apache logs as well as in the application tracking. This does make sense since from Apache's perspective the proxy is the actual client and not the real remote client. I found however that it was possible to get around this. Apparently there are several Apache modules that can do this, however I did not want to build from sources if I could avoid it. So I choose the one that was already available in the EPEL repository.

I already had the EPEL repository registered on the server but for those that does not you can use the following commands to register it.
rpm -Uvh http://ftp.crc.dk/fedora-epel/6/i386/epel-release-6-8.noarch.rpm
Now install the module
yum install mod_extract_forwarded
After installing the module you need to register an allowed forwarder (ie. the IP of your proxy). In all my logs the ip 127.0.0.1 was registered as the client of every request so adding that did the trick.
echo "MEFaccept 127.0.0.1" >> /etc/httpd/conf.d/mod_extract_forwarded.conf
service httpd restart
Recheck your webserver/application logs, this worked for me.

Thursday, July 25, 2013

Using Nginx to reverse proxy a secure apache site that is using socket.io/node.js/websockets

Challenge:
Firewalls can cause challenges, by blocking the ports that you want to use for websockets.

Many firewalls in use today are so called stateful firewalls and in short their function can be described as follows

Stateful firewalls only verify that a packet correlates to an existing, unclosed, connection. It tracks the state of the connection (opening, open, closing, closed) hence the name. When it detects a packet is part of an already open, authorized connection, it can short circuit all of the other rule checks and let the packet through.
(
http://stackoverflow.com/questions/1967943/will-html5-websockets-be-crippled-by-firewalls)
Most sites that are using node.js for websockets are also utilizing the library socket.io

A good list outlining some of the challenges when combining websockets/socket.io with various firewalls can be found at Socket.IO and firewall software.

So the question is how do you get your website running in spite of corporate firewall blocking various ports?


Possible solutions:

  1. ask the IT department to open the needed ports (in my experience hardly likely unless #2 fails)
  2. piggy-back on one of the existing established connections using http (port 80) or https (port 443) and hope the firewall is not a deep packet inspection based firewall.
 
Since I seldom have luck with suggestion number 1, I am not able to offer any guidance that is likely to help you succeed.

Suggestion number 2 however we can achieve that by letting the websockets use port 80/443 and let the website use the same port. You cannot however run 2 different services on the same port, so you cannot do this in a straightforward manner. The answer is a websocket aware reverse proxy.

There are quite a few different setups that can solve this. Below you can find 3 ways I investigated when I needed to solve this:


Setup
  • CentOS based Linux distribution
  • existing website installed as a Apache virtual host running on port 80 and 443
  • existing node.js socket.io based application running on port 8081


(setup before reverse proxy)


Please find an image of the wished for setup here below.

 
We need to do the following to achieve the reverse proxy setup
  1. Install Nginx
  2. Remove http -> https rewrite and ssl configuration from Apache as well as change Apache from listening on port 80/443 to listen on port 81
  3. Configure http -> https rewrite and reverse proxy in Nginx
  4. Change the socket.io node.js application to just use ws instead of wss (since Nginx is the SSL termination proxy now)
  5. Change the client application from using port 8081 to using 80/443

1. Install Nginx
RHEL / Centos 6: Install Nginx Using Yum Command
 


2. Remove http -> https rewrite and ssl configuration from Apache as well as change Apache from listening on port 80/443 to listen on port 81

Open the virtual hosts file for the site
vi /etc/httpd/conf.d/www_somewhere_com.conf
Change your virtual hosts file from the following 

# Catch those that are trying to access the site using http
# redirect to https
<VirtualHost *:80>
  ServerName "www.somewhere.com"
  DocumentRoot /var/www/sites/www.somewhere.com/www
  DirectoryIndex index.html index.php
  ErrorLog logs/www.somewhere.com-error_log
  CustomLog logs/www.somewhere.com-access_log common
  RewriteEngine On
  RewriteCond %{HTTPS} !=on
  RewriteRule ^(.*) https://www.somewhere.com [R,L]
</VirtualHost>

# Main site
<VirtualHost *:443>
  ServerName "www.somewhere.com"
  DocumentRoot /var/www/sites/www.somewhere.com/www
  DirectoryIndex index.html index.php
  ErrorLog logs/www.somewhere.com-error_log
  CustomLog logs/www.somewhere.com-access_log common

  SSLEngine on
  SSLCertificateFile /etc/pki/tls/certs/www_somewhere.com.crt
  SSLCertificateKeyFile /etc/pki/tls/private/www_somewhere.com.key
  SSLCertificateChainFile /etc/pki/tls/certs/Intermediate_Certificate.crt

  <Directory /var/www/sites/www.somewhere.com/www>
    Order Deny,Allow
    Allow from all
  </Directory>
</VirtualHost>

to the following

# Main site
<VirtualHost *:81>
  ServerName "www.somewhere.com"
  DocumentRoot /var/www/sites/www.somewhere.com/www
  DirectoryIndex index.html index.php
  ErrorLog logs/www.somewhere.com-error_log
  CustomLog logs/www.somewhere.com-access_log common

  <Directory /var/www/sites/www.somewhere.com/www>
    Order Deny,Allow
    Allow from all
  </Directory>
</VirtualHost>

vi /etc/httpd/conf/httpd.conf
Look for the "Listen 80" in the file and change it to "Listen 81"
Save the file


3. Configure http -> https rewrite and reverse proxy in Nginx
vi /etc/nginx/conf.d/default.conf


replace the deafult content with this


Please note that in apache config you have a seperate intermidiate certificate and the site certificate. In Nginx you need to concatenate the primary certificate file (your_domain_name.crt) and the intermediate certificate file (DigiCertCA.crt) into a single pem file by running the following command:

cat DigiCertCA.crt >> your_domain_name.crt


# Listeners
# nginx         :80         responsible for redirecting http://www.somewhere.com to https://www.somewhere.com 
# nginx         :443        responsible for handling all trafic on the https://www.somewhere.com site
#                           acts as a reverse proxy for 
#                           /socket.io/     -> port 127.0.0.1:8081  (node.js websocket server)
#                           /               -> port 127.0.0.1:81    (apache  main site)
#
# node.js   :8081   node.js websocket server
# apache    :81     apache  main site


upstream www.somewhere.com {
    server 127.0.0.1:81;
    least_conn;
}

# nginx         :80         responsible for redirecting http://www.somewhere.com to https://www.somewhere.com 
server {
    listen 80;
    server_name  www.somewhere.com;
    rewrite ^(.*) https://www.somewhere.com$1 permanent;
}

# nginx         :443        responsible for handling all trafic on the https://www.somewhere.com site
#                           acts as a reverse proxy for 
#                           /socket.io/     -> port 127.0.0.1:8081  (node.js  websocket server)
#                           /               -> port 127.0.0.1:81    (apache  main site)
server {
    listen 443 ssl;
    server_name www.somewhere.com;

    ssl_certificate     /etc/pki/tls/certs/www_somewhere.com.crt;
    ssl_certificate_key /etc/pki/tls/private/www_somewhere.com.key;
    ssl_protocols       SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         RC4:HIGH:!aNULL:!MD5;

    proxy_redirect     off;
    proxy_set_header   Host             $host;
    proxy_set_header   X-Real-IP        $remote_addr;
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

    # Path based websocket proxy location for nginx
    # to handle the  reverseproxying
    #
    # inital connect url: https://www.somewhere.com:8081/socket.io/1/?t=1374751734771
    # websocket url     :   wss://www.somewhere.com:8081/socket.io/1/websocket/x55Ch0B-SCACmDpW8rZd   
    location /socket.io/ {
        proxy_pass http://localhost:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }   

    # Path based site proxy location for nginx
    # to handle the main apache  site
    location / {
        proxy_pass http://localhost:81;
    }   
}


4. Change the socket.io node.js application to just use ws instead of wss (since Nginx is the SSL termination proxy now)
Change you node.js socket.io application from this


var fs = require('fs');
var sslCertificate = {
    key: fs.readFileSync(config.sslCertificate.key),
    cert: fs.readFileSync(config.sslCertificate.cert),
    ca: fs.readFileSync(config.sslCertificate.ca)
};

// socket.io wss (websocket secure)
var io = require('socket.io').listen(8081, sslCertificate);
...

to the following

var fs = require('fs');
var sslCertificate = {
//    key: fs.readFileSync(config.sslCertificate.key),
//    cert: fs.readFileSync(config.sslCertificate.cert),
//    ca: fs.readFileSync(config.sslCertificate.ca)
};

// socket.io ws (websocket)
var io = require('socket.io').listen(8081, sslCertificate);
...


5. Change the client application from using port 8081 to using 80/443

Find the location in your client code where you are connecting to the websocket/socket.io application/server and change it from hardcoding the port in the url to just omitting it

So instead of (in a angular.js application / javascript single page application) doing this


var socketIO = function(data){
    // force https / wss
    var hostURL = window.location.href.replace('/'+window.location.hash, '8081').replace('http:', 'https:');

    ...
    
    try {
        var socket = io.connect(hostURL, {'sync disconnect on unload':true});
        ...
    }
    catch(e){
        console.log('Error connecting');
        ...
    }
    ...
}

do something like this
var socketIO = function(data){
    // force https / wss
    var hostURL = window.location.href.replace('/'+window.location.hash, '').replace('http:', 'https:');

    ...
    
    try {
        var socket = io.connect(hostURL, {'sync disconnect on unload':true});
        ...
    }
    catch(e){
        console.log('Error connecting');
        ...
    }
    ...
}

run the following command to check if the services are running on the correct ports

#netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address               Foreign Address             State       PID/Program name
tcp        0      0 0.0.0.0:80                  0.0.0.0:*                   LISTEN      13146/nginx
tcp        0      0 0.0.0.0:8081                0.0.0.0:*                   LISTEN      13225/node
tcp        0      0 127.0.0.1:81                0.0.0.0:*                   LISTEN      13190/httpd
tcp        0      0 0.0.0.0:443                 0.0.0.0:*                   LISTEN      13146/nginx
...

Finally some before and after pictures from chrome
Before




After


Monday, July 01, 2013

Compressed Node.js install on CentOS 6.4

yum install -y make gcc cc gcc-c++ wget; cd /usr/src; wget http://nodejs.org/dist/v0.10.12/node-v0.10.12.tar.gz; tar zxf node-*.tar.gz; cd node-v*; ./configure; make; make install;

Wednesday, June 19, 2013

Installing ejabberd2 on CentOS and configure a virtual host on Apache2

1. Enable epel yum repository

Centos 5.x
wget http://dl.fedoraproject.org/pub/epel/5/x86_64/epel-release-5-4.noarch.rpm
sudo rpm -Uvh epel-release-5*.rpm

Centos 6.x
wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
sudo rpm -Uvh epel-release-6*.rpm

2. Install ejabberd2
yum --enablerepo=epel install ejabberd

3. Start the ejabberd2 service
service ejabberd start

4. Edit the virtual hosts configuration file and add
  ProxyPass /http-bind  http://127.0.0.1:5280/http-bind/
  ProxyPassReverse  /http-bind   http://127.0.0.1:5280/http-bind/
  Header set Access-Control-Allow-Origin "YOUR_VIRTUAL_SERVER_HOSTNAME"

(please note that using "Header set Access-Control-Allow-Origin" enables Cross-origin resource sharing and using a wildcard as origin should not normally be used)

This is generally not appropriate. The only case where this is appropriate is when a page or api response is considered completely public content and it is intended to be accessible to browsable to everyone. Including any code on any site.
(http://en.wikipedia.org/wiki/Cross-origin_resource_sharing)

5. Test configuration
service httpd configtest

6. Reload apache configuration
service httpd reload

Since ejabberd comes with http-bind enabled out of the box
 
{5280, ejabberd_http, [
 %%{request_handlers,
 %% [
 %%  {["pub", "archive"], mod_http_fileserver}
 %% ]},
 captcha,
 http_bind,
 http_poll,
 %%register,
 web_admin
]}

you should now be able to access the XMPP over BOSH (XEP-0206) server in your browser looking somewhat like below.


 

Monday, June 03, 2013

Xamarin Mono.Droid Receiving different bluetooth devices data records

When you are working with several different Bluetooth devices (from different manufacturers) you will have to be able to handle different types of data records. The official Android Bluetooth sample ported for Xamarin/Mono.Droid can be found at BluetoothChatService.cs. This was the code I used for inspiration when I built this app.
I could however not really get the code to work as I wanted and the problem was located in the following method Run().
public override void Run ()
{
 Log.Info (TAG, "BEGIN mConnectedThread");
 byte[] buffer = new byte[1024];
 int bytes;

 // Keep listening to the InputStream while connected
 while (true) {
  try {
   // Read from the InputStream
   bytes = mmInStream.Read (buffer, 0, buffer.Length);

   // Send the obtained bytes to the UI Activity
   _handler.ObtainMessage (BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
    .SendToTarget ();
  } catch (Java.IO.IOException e) {
   Log.Error (TAG, "disconnected", e);
   _service.ConnectionLost ();
   break;
  }
 }
}
The problem is the following call
// Send the obtained bytes to the UI Activity
_handler.ObtainMessage (BluetoothChat.MESSAGE_READ, bytes, -1, buffer).SendToTarget ();
which might not always send back the entire device record to the handler. This is caused by the various devices implementation of how to send the data record back as a byte stream to the mobile device/phone might not finish, before the while loop is starting another iteration, as well as the multithreading and the context switches that occurs in the BluetoothChatService.

What I discovered while testing was that for some devices it was working out of the box with the default code in the Run method above. However on some more complex devices which were sending larger data records back, it never seemed to work.

After spending some time analyzing the problem - being new to the bluetooth scene and Mono.Droid development - I found that data records got chopped into pieces in the while(true) loop in the above Run() method. Assuming that a full data record was 65 bytes, sometimes the code would pass 13 bytes to the SendToTarget() method. The next iteration might yield 31 bytes and the last 21 bytes was yielded on the 3rd pass. 

The number of bytes returned on each iteration was never the same and there was no specific pattern to this. To be able to handle this for different devices sending data in different ways I needed some kind of smart buffering. I came up with the following.
public override void Run ()
{
 Log.Info (TAG, "BEGIN mConnectedThread");
 byte[] buffer = new byte[1024];
 int bytes;
 
 // Keep listening to the InputStream while connected
 while (true) {
  try {
   // Read from the InputStream
   bytes = mmInStream.Read(buffer, 0, buffer.Length);

   var messagePart = new byte[bytes];
   Array.Copy(buffer,messagePart, bytes);

   // Send the obtained bytes to the UI Activity
   _handler.ObtainMessage(MESSAGE_READ, bytes, -1, messagePart)
    .SendToTarget();
  } catch (Java.IO.IOException e) {
   Log.Error (TAG, "disconnected", e);
   _service.ConnectionLost ();
   break;
  }
 }
}
The new code is only a little bit different in that I am no longer sending back the buffer but instead only the partial message (messagePart).

In the handler that handles the read bytes I changed the implementation from what can be found here BluetoothChat.cs.
case MESSAGE_READ:
 byte[] readBuf = (byte[])msg.Obj;
 // construct a string from the valid bytes in the buffer
 var readMessage = new Java.Lang.String (readBuf, 0, msg.Arg1);
 bluetoothChat.conversationArrayAdapter.Add (bluetoothChat.connectedDeviceName + ":  " + readMessage);
 break;
to the following code

case BluetoothService.MESSAGE_READ:
 byte[] readBuf = (byte[])msg.Obj;
 ParseBluetoothDeviceReading(readBuf);
 break;
and the ParseBluetoothDeviceReading() method looking like this
public void ParseBluetoothDeviceReading (byte[] readBuffer)
{
 if (readBuffer == null || readBuffer.Length <= 0)
  return;

 var deviceRecord = _zephyrResponseParser.ParseDeviceResponse(readBuffer);
 _data.LungFunction= deviceRecord.LungFunction;
 lblValue.Text = deviceRecord.LungFunction.ToString("0.00");
}
the data record buffer is implemented in the _zephyrResponseParser which is of type IDeviceResponseParser. The interface IDeviceResponseParser looks like the following

public interface IDeviceResponseParser<TDataRecord>
{
 TDataRecord ParseDeviceResponse(byte[] response);
}
and finally the actual device specific data record parser for the zephyr device.

using System.Collections.Generic;

namespace Droid.Devices.Zephyr
{
 public class ZephyrResponseParser : IDeviceResponseParser<ZephyrRecord>
 {
  public const int DeviceMessageLength = 65;
  
  private List<byte> _messageBuffer = new List<byte>();

  public ZephyrRecord ParseDeviceResponse(byte[] response)
  {
   // Since the data (byte[]) that we are getting is retreived from a BluetoothNetworkStream there are no guarantees that we will get the full message in one go.
   // For that reason we need to buffer it our selves until whe have a full message
   _messageBuffer.AddRange(response);
   if (_messageBuffer.Count < DeviceMessageLength)
   {
    // Not full message return
    return new ZephyrRecord();
   }

   // Full message, pass on to temp so we can reset the local messageBuffer for the next
   // device message
   var temp = _messageBuffer.ToArray();

   // Reset buffer
   _messageBuffer.Clear();
   
   // Now process the full message
   response = temp;
   if (response.Length >= DeviceMessageLength)
   {
    var deviceRecord = ExtractDataFromRawByteStream();
    return new ZephyrRecord { LungFunction = deviceRecord.LungFunction };
   }
   return new ZephyrRecord();
  }
 }

 ...
 
 public class ZephyrRecord
 {
  public float LungFunction { get; set; }
 }
}
This has proven to work so far, without any known problems, for several different devices that are working very differently, has different manufacturers and are measuring different medical parameters.

Sunday, June 02, 2013

Android 4.0.3 and 4.0.4 bluetooth pairing needed on every connection

While developing a cross mobile device app using Xamarin, I ran into a strange problem. Behind the cross device abstraction, that was utilized in both the Android and the iOS version of the app, the Android specific code had a problem. The problem manifested it self on one Samsung Galaxy 10.1 tablet running 4.0.3, as well as one running 4.0.4. The problem was not present on the reference "lowest common denominator tablet" (this was also a Samsung Galaxy 10.1 tablet, just running Android 3.2).

After some Google searches and not really coming up with anything specific, I found this link which was actually showing me the solution, although I did not know it at the time.

Android Programmatically Bluetooth Pairing

Our app is using a somewhat modified version of the official Android Bluetooth sample ported to monodroid, it can be found here BluetoothChat.

The problem showed out to be this line "tmp = device.CreateRfcommSocketToServiceRecord (MY_UUID);"

changing this to 

tmp = device.CreateInsecureRfcommSocketToServiceRecord (MY_UUID);

solved the problem for 3 different devices for me.

Some more Googling gave me the apparent reason behind this. There seems to be a bug in Android 4.0.3 and 4.0.4+ that is causing the Bluetooth pairing to be forgotten.

This is documented amongst others on Bluetooth connection on Android ICS not possible.

I spent quite some time trying to figure out what was wrong with the code, being in the mindset "I must be doing something wrong" and being a bit reluctant to start the "blamegame".

I hope this might help someone else out there.

Below are a few links that I went through in my hunt for a solution, they are all one way or another corroborating my findings.

ICS can't pair BT devices without PIN 
Bluetooth RFCOMM Server Socket no longer connects properly to embedded device on ICS 4.0.3

Wednesday, May 22, 2013

Runtime performance friendly AngularJs localization / translation using buildscript and Yii Framework + Invalidating the browser cache

Lately I have been working on a new web based single page application. The technologies and frameworks in use are:
One of the design challenged were - how do we approach webpage localization?

The following requirements were given:

  • It should incur as small run time overhead as possible, preferably none
  • The localizable strings should be maintained in text files to allow for external translators, without the overhead of having a full translation system installed and configured
  • The translation system should support pluralization

The solution that were chosen is a deployment/build step implemented in the Yii Framework that both translates and statically renders AngularJS views. The translation engine that is used is also included in the Yii Framework.

The below image shows a module "logbook" and the views needed for some simple CRUD functionality. This image from the filesystem is from the repository (i.e. before the build script has run).



The image below shows how the filesystem looks after the buildscript has run (at the hosting server)


As we can see the build script has added a directory for each supported language with the 2 letter language abbreviation according to ISO 639-1.

Lets have a look at first the english and then the danish version of the actual view when it is presented on the browser screen.


Lets have a look at the html for the angular view.

<div class="span12" id="logbook_edit" ng-controller="logbook.EditController">
    <div id="content">
        <h1><?=LogbookModule::t('Log');?></h1>
        <div ng-include src="'assets/__REVNO__/app/views/'+language+'/logbook/logbook_edit_toolbar.htm'"></div>    
        <form id="frm_logbook_edit" data-id="{{log.id}}" class="form-horizontal">
        <p class="note"><?=Yii::t('sitewide',"msg_field_with_asterisk_are_required");?></p>
        <div class="control-group">
            <label class="control-label required" for="logdate"><?=LogbookModule::t('Date');?></label>
            <div class="controls">
                <input type="text" id="logdate" name="logdate" ng-model="log.logdate" bs-datepicker data-date-format="Yii.user.preferred_date_format.toString().toLowerCase()" />
                <span class="help-inline"></span>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label required" for="title"><?=LogbookModule::t('Title');?></label>
            <div class="controls">
                <input type="text" name="title" id="title" ng-model="log.title">
                <span class="help-inline"></span>
            </div>
        </div>        
        <div class="control-group">
            <label class="control-label required" for="mood"><?=LogbookModule::t('Mood');?></label>
            <div class="controls">
                <div class="btn-group mood-button-group" ng-model="log.mood" data-toggle="buttons-radio" bs-buttons-radio>
                  <button type="button" value="happy" class="btn happy" style="background-image:url(assets/__REVNO__/app/img/happy-icon.png);"></button>
                  <button type="button" value="sad" class="btn sad" style="background-image:url(assets/__REVNO__/app/img/sad-icon.png);"></button>
                  <button type="button" value="angry" class="btn angry" style="background-image:url(assets/__REVNO__/app/img/angry-icon.png);"></button>
                  <button type="button" value="ill" class="btn ill" style="background-image:url(assets/__REVNO__/app/img/ill-icon.png);"></button>
                </div>                
                <input type="hidden" id="mood" name="mood">
                <span class="help-inline"></span>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label required" for="note"><?=LogbookModule::t('Note');?></label>
            <div class="controls">
                <textarea cols="" rows="" id="note" name="note" ng-model="log.note"></textarea>
                <span class="help-inline"></span>
            </div>
        </div>
        <div ng-include src="'assets/__REVNO__/app/views/'+language+'/logbook/logbook_edit_toolbar.htm'"></div>
        </form>
    </div>
</div>

There are a few things that are special in this HTML code
  •  "__REVNO__" placeholder which at buildscript run time is replaced by the actual revision identifier from the source code repository. This allows us to optimize the content headers, for maximum browser caching while at the same time ensuring that the end user gets the latest version when we update to a new version on the server. Controlled cache invalidation/busting
  •  "<?=LogbookModule::t('Note');?>" is the Yii Framework translation method/function t();
  • the angular scope variable "language" that is part of the logged in users context, this is represented by an 2 letter language abbreviation.
Lets have a look at how English translation string file look (standard Yii Framework messages file)

<?php
return array(
    'Date'=>'Date',
    'Title'=>'Title',
    'Mood'=>'Mood',
    'Note'=>'Note',
    'Log'=>'Log',
    'Edit'=>'Edit',
    'Delete'=>'Delete',
    'Create'=>'Create',
    'List'=>'List',
    'Save'=>'Save',
    'Logbook'=>'Logbook',
    'Created_by'=>'Created by',
);

and then how the translated view ends up looking after running the buildscript

<div class="span12" id="logbook_edit" ng-controller="logbook.EditController">
    <div id="content">
        <h1>Log</h1>
        <div ng-include src="'assets/1932/app/views/'+language+'/logbook/logbook_edit_toolbar.htm'"></div>    
        <form id="frm_logbook_edit" data-id="{{log.id}}" class="form-horizontal">
        <p class="note">Fields with <span class="required">*</span> are required.</p>
        <div class="control-group">
            <label class="control-label required" for="logdate">Date</label>
            <div class="controls">
                <input type="text" id="logdate" name="logdate" ng-model="log.logdate" bs-datepicker data-date-format="Yii.user.preferred_date_format.toString().toLowerCase()" />
                <span class="help-inline"></span>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label required" for="title">Title</label>
            <div class="controls">
                <input type="text" name="title" id="title" ng-model="log.title">
                <span class="help-inline"></span>
            </div>
        </div>        
        <div class="control-group">
            <label class="control-label required" for="mood">Mood</label>
            <div class="controls">
                <div class="btn-group mood-button-group" ng-model="log.mood" data-toggle="buttons-radio" bs-buttons-radio>
                  <button type="button" value="happy" class="btn happy" style="background-image:url(assets/1932/app/img/happy-icon.png);"></button>
                  <button type="button" value="sad" class="btn sad" style="background-image:url(assets/1932/app/img/sad-icon.png);"></button>
                  <button type="button" value="angry" class="btn angry" style="background-image:url(assets/1932/app/img/angry-icon.png);"></button>
                  <button type="button" value="ill" class="btn ill" style="background-image:url(assets/1932/app/img/ill-icon.png);"></button>
                </div>                
                <input type="hidden" id="mood" name="mood">
                <span class="help-inline"></span>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label required" for="note">Note</label>
            <div class="controls">
                <textarea cols="" rows="" id="note" name="note" ng-model="log.note"></textarea>
                <span class="help-inline"></span>
            </div>
        </div>
        <div ng-include src="'assets/1932/app/views/'+language+'/logbook/logbook_edit_toolbar.htm'"></div>
        </form>
    </div>
</div>

Now on to the actual build script which is implemented as a Yii CConsoleCommand

<?php
class BuildReleaseCommand extends CConsoleCommand
{
    var $careSupportedLanguages = array ('EN','DA');
    var $packageRevisionInSvn;
    
    public function actionBuild($siteDirectory, $subversionRepoUrl='https://your_repo_url_here', $svnCheckout=0) 
    {
        $packageRevisionInSvn = $this->getLatestRevisionNumberInSvn($subversionRepoUrl);
        $this->packageRevisionInSvn = $packageRevisionInSvn;
        
        // Delete same revsion folder to replace with newer one
        `rm -rf $siteDirectory/assets/$packageRevisionInSvn`;
        
        // Create php file to store revision number, it will be used in all php file where revision number is required
        $revisionFileContent = '<?php $revision = '.$packageRevisionInSvn.'; ';
        file_put_contents("$siteDirectory/revision.php", $revisionFileContent);

        if($svnCheckout){
            $this->checkoutLatestVersionFromSvn($subversionRepoUrl, $siteDirectory);
        }
        $this->insertRevisionNumberInFolderNames($siteDirectory, $packageRevisionInSvn);
    
        // Delete all directory other then en& da in $siteDirectory/assets/{$this->packageRevisionInSvn}/app/views/
        `rm -rf $siteDirectory/assets/{$this->packageRevisionInSvn}/app/views/*`;
        
        foreach($this->careSupportedLanguages as $key=>$language) {
            Yii::app()->language=strtolower($language);
            $this->translateViews($siteDirectory.'/assets/__REVNO__/app/views', $siteDirectory. '/assets/'.$this->packageRevisionInSvn.'/app/views');
        } 

        $this->insertRevisionNumberInViewUrls($siteDirectory.'/assets/'.$packageRevisionInSvn, $packageRevisionInSvn);
    }

    private function getLatestRevisionNumberInSvn($packageUrl) {
        return trim(`svn info --non-interactive --username YOUR_USERNAME --password YOUR_PASSWORD $packageUrl | grep 'Last Changed Rev' | head -1 | grep -Eo "[0-9]+"`);
    }

    private function checkoutLatestVersionFromSvn($packageUrl, $svnExportDirectory){
        $output = `svn export --force --non-interactive --username YOUR_USERNAME --password YOUR_PASSWORD $packageUrl $svnExportDirectory`;
        $output = `cd $svnExportDir$package`;
    }

 /**
 * Recurse through the filessystem to process all view html files
 * Feature: Translation 
 */
    private function translateViews($srcPath, $destPath){
        $ignoreFiles = array( '.', '..' ); 
        $dh = @opendir( $srcPath ); 
        while( false !== ( $file = readdir( $dh ) ) ){ 
            if( !in_array( $file, $ignoreFiles) ){ 
                if( is_dir( "$srcPath/$file" ) ){ 
                    $this->translateViews( "$srcPath/$file", "$destPath/$file" ); 
                } else { 
                    if (preg_match('/^.*\.(htm)$/i',$file)) {
                        $translatedView = $this->translateView("$srcPath/$file");
                        $this->saveTranslatedView("$destPath/$file", $translatedView);
                    }
                } 
            } 
        } 
        closedir( $dh ); 
    } 

 /**
 * Actual translation of the html view file
 * Feature: Translation 
 */
    private function translateView($fileFullPath){
        $module = $this->getModuleName($fileFullPath);
        $controller = new CController($module,new CWebModule($module,null));
        return $controller->renderInternal($fileFullPath,null,true);
    }

 /**
 * Save the processed view file under the ISO 639-1 2 letter language code directory
 * Feature: Translation 
 */
    private function saveTranslatedView($fileFullPath, $translatedView) {
        $translatedViewFile = str_replace('views','views/'.Yii::app()->language, $fileFullPath);
        $saveDir = dirname($translatedViewFile);
        if (!file_exists($saveDir)){
            mkdir($saveDir,0755, true);
        }
        file_put_contents($translatedViewFile, $translatedView);
    }

    /**
 * Replace the __REVNO__ placeholder in urls in the Angular views so it points to the correct revision number
 * Feature: Cachebusting
 */
 private function insertRevisionNumberInViewUrls($svnExportDirectory, $packageRevisionInSvn) {
        $output = `find $svnExportDirectory \( -iname "*.htm" -or -iname "*.js" -or -iname "*.php" \) -print | xargs sed -i 's/__REVNO__/$packageRevisionInSvn/g'`;
    }

    /**
 * Copy and rename the __REVNO__ folder to the actual revision number we are parsing
 * Feature: Cachebusting
 */
 private function insertRevisionNumberInFolderNames($svnExportDirectory, $packageRevisionInSvn) {
        $output = `cp -Rf $svnExportDirectory/assets/__REVNO__  $svnExportDirectory/assets/$packageRevisionInSvn`;
    }

    public function getModuleName($viewPath)
    {
        $dirs = explode("/", $viewPath);
        $passedViews = false;
        $module = "";
        foreach ($dirs as $key=>$dir)
        {
            if ($passedViews)
            {
                $module = $dir;
                return $module;
            }
            if ($dir == "views") {
                $passedViews = true;
                continue;
            }    
        }   
    }

}
?>
The script is commented to a fair degree, suffice to say that I am using the internal view renderer in the Yii Framework to do the heavy lifting for the translation, it knows where to find the language string files and to do the translation and pluralization. 

I am using the build script together with migration for auto-deployment to our test server like this:


cd /YOUR_WEBSITE_DIR; svn update --username YOUR_USERNAME --password YOUR_PASSWORD; chown -R apache:apache /YOUR_WEBSITE_DIR; protected/yiic migrate --interactive=0; protected/yiic buildrelease build --siteDirectory=/YOUR_WEBSITE_DIR/

Wednesday, February 06, 2013

Old HTC Desire phone not booting

I have an old HTC Desire phone that is not able to boot ... again. Last time this happened is about a year ago. Both back then and this time I had to spend the time all over since I could not remember what made it work last time. Thus the rationale for this posting.

What happens is that suddenly the phone just runs out of battery and then it will not boot again.It is stuck on the white HTC screen and you cannot turn it off using the power button. To turn it off you need to pull out the battery.

See more on a similar or the same problem here: Phone stuck on fastboot/hboot, buttons unresponsive

Find a available free SD card stick it into another phone and format the SDCard to FAT32 I used Linux for this since I have had problems doing this on windows for some unknown reason.

On linux you do the following


  1. Plug in your phone USB cable and wait until your OS recognizes the device
  2. Change to root user
  3. type mount and locate the device name of the SD Card and the directory on which it is mounted (typically something like /dev/sdb1 and /media/SDCARD_ID)
  4. type umount /media/SDCARD_ID
  5. now we are going to format the SDCARD (if you have any data on the card you will loose them) type mkdosfs -I -F32 /dev/YOUR_FOUND_DEVICE_NAME_FROM_STEP_3

    (please yet again note that the device name that I am refering to /dev/sdb1 might refer to a different drive on your system, when you format this drive all data will be deleted! Make sure you are pointing to the correct drive!)
  6. Now you have a SDcard that is ready
  7. Download the RUU file (officially release ROM from the manufacturer) I am using this one RUU_Bravo_Froyo_HTC_WWE_2.29.405.14_Radio_32.49.00.32U_5.11.05.27_release_224699_signed.exe
  8. This is the official way to install software using a Windows  OS (always make sure that you virus scan exe files downloaded from the internet, and also check the MD5 checksum to make sure that it indeed is the file that you expect it to be).
  9. If you phone can boot then you can re-flash on Windows by just running the exe with the phone connected by the USB cable.
  10. My phone however is missing the PB99IMG.zip file and the only way that you can reinstall the phone OS is by.
  11. Start the exe file, and follow the following screenshots


  12. Now do not go further, download and install the excellent tool Process Explorer 
  13. Start Process Explorer and find out where the temporary files from the RUU...exe file has been extracted.
  14. Select the exe process that you have just started
  15. Make sure that the lower process details pane is open
  16. Find the directory that contains Disk1. In either this directory or one of the most recent directories in the indicated temp folder look for a file named ROM.zip
  17. When you find it, copy it to the newly formatted SDCard
  18. Rename the file to fit you specific image (it differs from phone model to phone model. Mine is called (please note uppercase letters) PB99IMG.zip
  19. Eject the spare phone
  20. Turn it off and take out the SDCard. Put it into the broken phone
  21. Plug in the broken phone to a charger or to the PC USB slot
  22. Turn on the broken phone while pressing volume down button and then pressing the power button until you get to the HBOOT screen
  23. Just leave the phone and after a while it will show loading image
  24. It will now ask you a number of times, if you want to flash the image and if you want to reboot the phone.
  25. After rebooting you phone you should be good to go