margenn

PHP: Lightweight detector of mobile devices, OSs & browsers

Posted on January 3, 2012 by

Share this!!

On a recent project I needed a way to know if a user is using a mobile device to visit the site I was building and, if so, I wanted a little more info about the device, OS and browser used.

Before baking my own solution, I checked existing solutions such as the great WURFL, well known framework’s own solutions like CodeIgniter’s and other quite good scripts like php-mobile-detect, detectmobilebrowsers.mobi or mobiforge.com.

None did what I needed in a simple and lightweight way:

WURFL

  • Not straightforward to implement. 
  • Device database updates have to be done manually and periodically (at least twice a year) to stay up to date (Ughh). 
  • Heavy on the server (poor performance). 

CodeIgniter’s user agent class

Works well for mobile detection but it does not tell you either the OS or browser used.

php-mobile-detect

I like this one a lot but, unfortunately it doesn’t detect every browser/os/device I wanted, it has some bugs and I haven’t tested its regular expressions.

detectmobilebrowsers.mobi

Very good script but, unfortunately it doesn’t detect every browser/os/device I wanted. However, I used it as a reference.

mobiforge.com

This script is more suited towards detecting old mobile devices (feature phones).

So in the end I made my own script.

All I wanted was a helper function that would execute once per page load and set some global variables.

Those of you who prefer or need an OOP approach should modify the php-mobile-detect class to your needs.

Here’s my function:

<?php
/*
 * Lightweight detector of mobile devices, OSs & browsers
 * Copyright 2012  Túbal Martín  (email: tubalmartin@gmail.com)
 * License: GPL2
 */

if ( ! function_exists('mobile_detector') )
{
    // Global vars
    $is_mobile = false;
    $is_iphone = $is_ipad = $is_kindle = false;
    $is_ios = $is_android = $is_webos = $is_palmos = $is_windows = $is_symbian = $is_bbos = $is_bada = false;
    $is_opera_mobile = $is_webkit_mobile = $is_firefox_mobile = $is_ie_mobile = $is_netfront = $is_uc_browser = false;


    function mobile_detector($debug = false)
    {
        global $is_mobile;

        // Check user agent string
        $agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';

        if (empty($agent)) {
            return;
        }

        $mobile_devices = array(
            'is_iphone' => 'iphone',
            'is_ipad' => 'ipad',
            'is_kindle' => 'kindle'
        );
        
        $mobile_oss = array(
            'is_ios' => 'ip(hone|ad|od)',
            'is_android' => 'android',
            'is_webos' => '(web|hpw)os',
            'is_palmos' => 'palm(\s?os|source)',
            'is_windows' => 'windows (phone|ce)',
            'is_symbian' => 'symbian(\s?os|)|symbos',
            'is_bbos' => 'blackberry(.*?version\/\d+|\d+\/\d+)',
            'is_bada' => 'bada'
        );
        
        $mobile_browsers = array(
            'is_opera_mobile' => 'opera (mobi|mini)', // Opera Mobile or Mini
            'is_webkit_mobile' => '(android|nokia|webos|hpwos|blackberry).*?webkit|webkit.*?(mobile|kindle|bolt|skyfire|dolfin|iris)', // Webkit mobile
            'is_firefox_mobile' => 'fennec', // Firefox mobile
            'is_ie_mobile' => 'iemobile|windows ce', // IE mobile
            'is_netfront' => 'netfront|kindle|psp|blazer|jasmine', // Netfront
            'is_uc_browser' => 'ucweb' // UC browser
        );
        
        $groups = array($mobile_devices, $mobile_oss, $mobile_browsers);
        
        foreach ($groups as $group) {
            foreach ($group as $name => $regex) {
                if (preg_match('/'.$regex.'/i', $agent)) {
                    global $$name;
                    $is_mobile = $$name = true;
                    break;
                }
            }
        }
        
        // Fallbacks
        if ($is_mobile === false) {
            $regex = 'nokia|motorola|sony|ericsson|lge?(-|;|\/|\s)|htc|samsung|asus|mobile|phone|tablet|pocket|wap|wireless|up\.browser|up\.link|j2me|midp|cldc|kddi|mmp|obigo|novarra|teleca|openwave|uzardweb|pre\/|hiptop|avantgo|plucker|xiino|elaine|vodafone|sprint|o2';
            $accept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : '';

            if (false !== strpos($accept,'text/vnd.wap.wml')
                || false !== strpos($accept,'application/vnd.wap.xhtml+xml')
                || isset($_SERVER['HTTP_X_WAP_PROFILE'])
                || isset($_SERVER['HTTP_PROFILE'])
                || preg_match('/'.$regex.'/i', $agent)
            ) {
                $is_mobile = true;
            }
        }

        // DEBUGGER OUTPUT
        if ($debug === true) {
            echo '<strong>User Agent: '.$agent.'</strong><br>';
            foreach ($GLOBALS as $k => $v) {
                if (strpos($k, 'is_') !== false) {
                    echo '<span style="color:'.($v ? 'green':'red').';">$'.$k.'</span><br>';
                }
            }
        }

    }

    // execute inmmediatly
    mobile_detector();

}

I have created a Gist so you can download or fork the code.

This function should only be called once per page load and preferably as soon as possible.

This function adds the following variables to the global scope:

Is it a mobile?

  • $is_mobile

Any famous device?

  • $is_iphone (Apple iPhone)
  • $is_ipad (Apple iPad)
  • $is_kindle (Amazon Kindle)

What mobile OS?

  • $is_android (Android OS)
  • $is_bada (Bada OS)
  • $is_bbos (Blackberry OS)
  • $is_ios (Apple iOS)
  • $is_palmos (Palm OS)
  • $is_symbian (Symbian OS)
  • $is_webos (Hp WebOS)
  • $is_windows (Windows Phone OS and older)

What mobile browser?

  • $is_firefox_mobile (Mozilla Fennec)
  • $is_ie_mobile (IE)
  • $is_netfront (NetFront)
  • $is_opera_mobile (Opera Mobile or Mini)
  • $is_uc_browser (UC Browser)
  • $is_webkit_mobile (Webkit)

The initial value of these variables is false.

You can test/debug the plugin results (debug mode) here.

If you need to debug the function, call it passing true as a parameter and the function will output the results.

If you use Wordpress to build websites and you’re reading this post let me tell you I already developed a plugin for Wordpress based on this function.

Share your thoughts!!

Tags: mobile detector device php browser

Wordpress: YouTube shortcode plugin. Anniversary.

Posted on January 3, 2012 by

Share this!!

Happy 2012 to everyone!!

As some of you may already know, it’s been a year now since I published my first ever plugin for Wordpress: Youtube shortcode

A year ago, version 1.0 was born and currently we’re at version 1.8.2. Much has been improved since its first release and that wouldn’t have been possible without the great community Wordpress has.

Version 1.0 was dead simple and lacked many features that today are present such as:

  • Fluid & static videos (responsive design approach)
  • Mobile & RSS readers compatibility
  • TinyMCE button
  • Support for many more custom and official parameters
  • Support for the AS2 video player

During the past year I also showed some love for good documentation and support creating a website for the plugin’s documentation. The purpose behind its creation was to give users the possibility to see the plugin in action. One month after its release Google Analytics reports it’s doing quite well with 130 unique visitors each day.

Wordpress.org gives both users and developers essential data about a plugin’s success such as the total number of downloads or the community rating and so far, people all over the world have downloaded the plugin 27.000 times and 10 people have given it a five stars rating.

All in all it’s been an exciting and very encouraging year!! A big thank you!!

What’s coming this year?

This year, I plan to release version 1.9 somewhere between january and march along with a Pro version.

Version 1.9 will give you the option to store default settings for every YouTube video on your website but will also allow you to override any default setting on a per video basis.

Pro version will offer a new and exciting performance mode that will allow your websites to load much much faster and its price will be quite affordable.

Stay tuned!!

Tags: wordpress youtube plugin shortcode

Javascript: getElementsByClassName cross-browser function

Posted on December 6, 2011 by

Share this!!

Recently, I needed a cross-browser implementation of the getElementsByClassName Javascript DOM method. I couldn’t use a JS library like jQuery because code size had to be as small as possible.

Fortunately, Robert Nyman developed back in 2008 a very good function to handle this issue.

I have refactored the function definition logic, optimized code as much as possible, fixed some bugs and added the missing querySelectorAll DOM method.

I’ve written some QUnit tests if you want to test wether it works as it should or not.

Parameters:

className One or several class names, separated by space. Multiple class names demands that each match have all of the classes specified. Mandatory.

tag Tag name of the elements to match. Optional.

elm Reference to a DOM element to look amongst its children for matches. Recommended for better performance in larger documents. Optional.

ChangeLog:

7 Dic 2011

  • Added a refactored and bugfixed XPath method. Robert Nyman’s version did not add normalize-space(@class) (space normalization) so his method wouldn’t return elements with a class attribute containing space characters other than whitespace (newlines for example). Also, namespace try/catch was removed from his version since we’re dealing with HTML documents and therefore it’s not necessary.
  • BugFix: Some elements (nodes) may not have a getElementsByClassName method, so we test for this. As a fallback document will be used. See this issue for more info.
  • BugFix for IE: Array.slice cannot be called on StaticNodeLists in IE. Instead, a regular loop is used to populate the array.

So here it is my 2011 version of the getElementsByClassName function:

        
/*
 *  Refactored & improved in 2011 by Tubal Martin (http://www.margenn.com)
 *  Unit tests: http://www.margenn.com/tubal/javascript/getelementsbyclassname.html
 *  Original version developed by Robert Nyman in 2008 http://code.google.com/p/getelementsbyclassname/
 */	
var getElementsByClassName = document.getElementsByClassName ?
    function (className, tag, elm) {
        var nodeElm = elm && elm.getElementsByClassName ? elm : document,
            elements = nodeElm.getElementsByClassName(className),
            nodeName = tag ? new RegExp("\\b" + tag + "\\b", "i") : null,
            rElements = [],
            i = 0, l = elements.length,
            current;
        for (; i < l; i++) {
            current = elements[i];
            if (!nodeName || nodeName.test(current.nodeName)) {
                rElements.push(current);
            }
        }
        return rElements;
    } : document.querySelectorAll ?
    function (className, tag, elm) {
        var elements = (elm || document).querySelectorAll((tag || "") + "." + className.split(" ").join(".")),
            rElements = [], 
            i = 0, l = elements.length;
        for (; i < l; i++) {
            rElements.push(elements[i]);
        }
        return rElements;
    } : document.evaluate ?
    function (className, tag, elm) {
        var classes = className.split(" "),
            classesToCheck = "",
            rElements = [],
            i = 0, l = classes.length, 
            elements, node;
        for(; i < l; i++){
            classesToCheck += "[contains(concat(' ', normalize-space(@class), ' '), ' " + classes[i] + " ')]";
        }
        elements = document.evaluate(".//" + (tag || "*") + classesToCheck, (elm || document), null, 0, null);
        while (node = elements.iterateNext()) {
            rElements.push(node);
        }
        return rElements;
    } :
    function (className, tag, elm) {
        tag = tag || "*";
        elm = elm || document;
        var classes = className.split(" "),
            classesToCheck = [],
            elements = tag == "*" && elm.all ? elm.all : elm.getElementsByTagName(tag),
            rElements = [],
            i = j = 0, 
            il = classes.length, 
            jl = elements.length,
            current, match, k, kl;
        for (; i < il; i++) {
            classesToCheck.push(new RegExp("(^|\\s)" + classes[i] + "(\\s|$)"));
        }
        kl = classesToCheck.length;
        for (; j < jl; j++) {
            current = elements[j];
            match = false;
            for (k = 0; k < kl; k++) {
                match = classesToCheck[k].test(current.className);
                if (!match) {
                    break;
                }
            }
            if (match) {
                rElements.push(current);
            }
        }
        return rElements;
    };

Tags: cross-browser dom getElementsByClassName javascript class xpath evaluate