Chrome Sniffer v0.2
This blog is the announcement of Chrome Sniffer v0.2. After v.0.1.7, I stopped development for a while. Yes, I was too busy, too much work, too many things to worry about. But I receive a lot of comments about the extension, many articles, many suggestions, etc. These encourage me a lot, so I spent last weekend to work with a new branch of Chrome Sniffer extension.
The biggest improvement in v0.2 will be the extension itself. In previous version, the extension mainly worked based on DOM or HTML content. so it can't perform detection very well, or some time detect wrong applications, especially with sites use Javascript compressor. The v0.2 will detect applications by Object directly, the result will be 100% correct for all javascript libraries, and it's compressor-compatible too.
v0.2 will be able to detect more applications. I have added Typekit to latest release, many people want to be able to detect Typekit so much. I plan to implement to give version detect and CSS framework detect in future version, you will likely to see these feature available on v0.2.2 or v0.2.3.
Chrome should update the extension itself, if you don't have it yet, you can install it here. I hope the v0.2 will be more useful. And it would be great to hear any suggestion or comment from any of you. Contact form is always open 
Improve Drupal permission system: administer taxonomy term & administer menu item
Whenever a new comer asks me about how to start with Drupal, I always warn him about Drupal's high learning curve and lack of usability before giving him advices. It's not only applied for developers, but also for end-users. This case is an example:
This time, my friend asks me about how to limit user access to taxonomy vocabulary term and menu item. In detail:
- User can create / edit / delete taxonomy term
- User can not create / edit / delete vocabulary
- User can create / edit / delete menu item
- User can not create / edit / delete menu
The reason he want to limit user access in that way because the person who manages this website is not well-educated in IT, so restrict user permission is a way to make him easier to work. But Drupal is lacking of this permission detail. We can use vocabulary permissions modules but this is unnecessary here. The question is why Drupal core developers do not develop this kind of permission for taxonomy and menu, like the way they do with node. Well, I'm not going to blame anyone, just a question and suggestion.
And here I solve the problem. I write a custom module, a very simple one, to alter menu permission to another functions, which added addition permission. The code is as simple as below:
function istar_admin_perm() { return array('administer taxonomy term'); } function istar_admin_menu_alter(&$menu) { $access_map = array( // taxonomy permission 'istar_admin_access_admin_taxonomy_term' => array( 'admin/content/taxonomy', 'admin/content/taxonomy/edit/term', 'admin/content/taxonomy/%taxonomy_vocabulary', 'admin/content/taxonomy/%taxonomy_vocabulary/add/term', ), ); foreach ($access_map as $access_callback => $access_menus) { foreach ($access_menus as $path) { $menu[$path]['access callback'] = $access_callback; } } } function istar_admin_access_admin_taxonomy_term($right) { return user_access($right) || user_access('administer taxonomy term'); }
I removed the part for menu item permission, but it would be the same with taxonomy term. This may raise a problem when we disable the module and the access callback function does not exist any more, I leave the `unisntall` part for you. If you have any suggestion on Drupal permission, and you need to help then you can always contact me in the contact form, or leave a comment below 
Integrate Drupal registration form with ubercart checkout
I had a very interesting experience with Drupal and Ubercart this week. One of my client ask me to implement a membership-based website, provide sophisticated content to premium users. The initial requirement is very simple: add a Package option in Sign up form, if user choose a premium package then require the user to complete the payment before accessing his account. This is a common requirement and any web developer would likely to run into one during his development life. The trouble with me is I never work with Ubercart before so I begun with some analyzing first. The below table is the my analysis about the requirements and challenges.
| Requirements | Challenges |
|
|
The hardest part is when and who will create the user: Drupal, Ubercart or a custom module. Ubercart can be discarded easily, since it has no integration with Profile module. Drupal won't help you too, because the user is only supposed to be created after the order is completed. I have also thought about using a custom role to grant special permission after the payment is completed, but if I do so, I will need to handle the `unpaid` user, provide a workflow for these kind of users to be able to upgrade again. I thought if I chose that way I would have to write more unnecessary code, so custom module is the last choice for me.
At first, I tried to use ctools to create a multi-step form with two steps, but I found it is too complicated to handle the Ubercart checkout form just as soon as I started the implementation. So I start with the simplest step: allow user to choose packages during registration. And I also want to prevent Drupal from creating user too, so I insert a custom handle at the top array of form #submit handlers.
/** * Get available packages */ function eiab_packages($id = NULL) { static $packages; if ($packages === null) { // load package nodes from database } return $id ? $packages[$id] : $packages; } function eiab_core_form_alter(&$form, &$state, $id) { if ($id == 'user_register') { $packages = eiab_packages(); $form['Package Selection'] = array( '#type' => 'fieldset', '#title' => t('Package selection'), '#weight' => 2, ); $options = array(); foreach ($packages as $package) { $options[$package->nid] = $package->title; } $form['Package Selection']['package'] = array( '#type' => 'radios', '#title' => t('Choose your package'), '#required' => TRUE, '#options' => $options ); array_unshift($form['#submit'], 'eiab_user_company_register'); } }
Note: make sure that your custom module handle is ran before any of other modules. I also used LogginToBoggan which created the user on form submition, we don't want to create user at this step. The form handle is simple too: store the submitted data into Session (or ctools object cache if you want), use Ubercart Cart API to add the package into the cart, and redirect to checkout form.
function eiab_user_company_register($form, &$state) { $package_id = $state['values']['package']; $package = eiab_packages($package_id); // Run it now so we can get the roles. autoassignrole_user('insert', $state['values'], $user, NULL); // Store submitted form information. $_SESSION['company_user'] = $state; uc_cart_remove_item($package_id); uc_cart_add_item($package_id, 1, NULL, NULL, FALSE); drupal_goto('cart/checkout'); }
One small thing to note here is you must run AutoAssignRole hook_user here to get user roles, because AutoAssignRole check the requesting URLs to assign roles so if later it won't work. I have to use AutoAssignRole because there are many types of users in the websites and this is just one type of users (think about content reader and content provider). If you don't use AutoAssignRole, things will be simpler.
The next step is to handle the checkout process, so I use hook_order from Ubercart to track order changes and perform necessary actions at this step.
function eiab_core_order($op, &$order, $arg = NULL) { if ($op == 'update') { // Retrieve submitted user form. $company = $_SESSION['company_user']; if ($company && $order->order_status == 'in_checkout') { $company_user_values = $company['values']; $company_user_values['checkout_pending'] = TRUE; $company_user = user_save(NULL, $company_user_values); db_query("UPDATE {uc_orders} SET primary_email = '%s', uid = %d WHERE order_id = %d", $company_user->mail, $company_user->uid, $order->order_id); unset($_SESSION['company_user']); // Update order object. $order->uid = $company_user->uid; watchdog('user', 'New user created: @user', array('@user' => $company_user->name), WATCHDOG_INFO); } if ($arg == 'completed') { $company_user = user_load($order->uid); if ($company_user && $company_user->status == 0 && $company_user->checkout_pending) { $pass = user_password(); user_save($company_user, array('status' => 1, 'checkout_pending' => NULL, 'pass' => $pass)); $company_user->password = $pass; // Mail token. _user_mail_notify('register_no_approval_required', $company_user); } } } }
Here you should understand two states of an order. If order state is change from in_checkout then the user just finishes the checkout workflow, this step is suitable to create the user now. And completed state means the order has finished, payment has been paid, it's a good time to activate user here. I also use a special flag checkout_pending to avoid updating the wrong user when others work on order.
So you can see that the implement is quite simple. The main trick is to prevent Drupal creating user, store submitted form and create user later. With this implementation, we don't need to integrate with other modules (in my case in Profiles, TermOfUse, LogginToBoggan, Mollom and many others in the future too). AutoAssignRole is a little special (buggy too, it should not depends on requesting URLs to assign user roles, that's why I need to run it hook manually).
If you choose to use a custom module to handle the workflow, make sure you configure the site properly, such as disable the `Add to cart` button for package product. It's a little bit confused if you can purchase the package directly, right ? I'm sure there may be simpler, easier way to address my issue, but in my case, it just works well. Do you have other solutions that works ? Share your thought.
Is msysgit a virus ?
If you are thinking about downloading and installing the latest version of msysgit at the moment (1.7.0.2-preview) then take my advice: do-not-install-git-cheetah (aka windows shell extension integration). I tried it last night, and feel very upset with this one.
When you start the installer of msysgit, it will ask you to choose a few settings, such as safe CRLF, or commit name & email. It's good, many of my friends have problem because msysgit enable safe CRLF by default. But the new thing in this version (or maybe in previous version) is Git Cheetah, and it's checked by default when you choose install options. Not talking about the uselessness and the ugliness of Git Cheetah (I'm using TortoiseGit, thanks!), once you install it, you will have to stick with this forever. You can not disable Git Cheetah, this option is not available in current version, and you can not remove Git Cheetah ether, even when you uninstall msysgit and install it again: Git cheetah is still there. If you uninstall msysgit and restart the computer, then you make a bad choice. Msysgit will begin to behave like a virus, keep popping up windows and stopping you from browsing (I'm not sure if Git Cheetah is guilty to my browser crashes, FF crashes many times when the virus is still in my computer).
The only way to `fix` Git Cheetah is to go to Registry editor, and search for the git cheetah (or you can use the dll name), then delete the registry folder which contains the result entry. After that, restart explorer to refresh Windows shell extension. If you are interested in fixing msysgit then the issue is already reported here, if not, you can just forget Git Cheetah and git happily with TortoiseGit. Let hope this bug is fixed soon.

Bonus: Say hi to git cheetah
ASP.NET Domain-based routing
In some case, you will want to have different routing for different domains, or sub-domains. Maarten Balliauw has already post a solution, but today I would like to share another solution on ASP.NET Domain-Based Routing. The idea is very simple. It works based on RouteData and RouteConstraint interface. It will be simpler than Maarten's solution but it does not support to extract route variable in domain name. This approach simply serve different page depend on the request domain.
At first, you should understand about RouteContraint, which use to validate request parameter. Here we will create a contraint to check for requested domain. Here is the source code:
public class DomainRouteConstraint : IRouteConstraint { #region IRouteConstraint Members public bool Match(System.Web.HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { // get hostname without port string requestDomain = httpContext.Request.Headers["host"].Split(':')[0]; string domain = (string)values["domain"]; return !String.IsNullOrEmpty(domain) && domain.Equals(requestDomain); } #endregion }
The class simply get host name from request header and check it with domain parameter in RouteData. So, later, if we want to create a domain-restricted route, we simply add domain parameter and domain contraint:
// custom domain route routes.Add("Domain1", new Route("{controller}/{action}/{id}", handler) { Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "", domain = "customdomain.com" }), Constraints = new RouteValueDictionary(new { domain = new DomainRouteConstraint() }), });
Everything should work like before, nothing changes except the route only match on specified domain. If you want to generate a route for specified domain, just pass domain as URL parameter. And if you want to generate absolute URL with domain name, you will have to write your custom method. Check it here, I leave this part for you so you can understand how this solution work. Let hope this post will be useful for you, or anyone else.