Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit d93168bad0e172b1e103f8a43f30d4f17785661b @talison talison committed Oct 28, 2009
Showing with 430 additions and 0 deletions.
  1. +67 −0 README.md
  2. +146 −0 lib/mobile-detect.rb
  3. +197 −0 test/test_mobile_detect.rb
  4. +20 −0 util/echo_env.rb
67 README.md
@@ -0,0 +1,67 @@
+Overview
+========
+
+`Rack::MobileDetect` detects mobile devices and adds an
+`X_MOBILE_DEVICE` header to the request is a mobile device is
+detected. The strategy for detecting a mobile device is as
+follows:
+
+1. Search for a 'targeted' mobile device. A targeted mobile device is
+ a device you may want to provide special content to because it has
+ advanced capabilities - for example and iPhone or Android phone.
+ Targeted mobile devices are detected via a `Regexp` applied against
+ the HTTP User-Agent header.
+
+ By default, the targeted devices are iPhone, Android and iPod. If
+ a targeted device is detected, the token match from the regular
+ expression will be the value passed in the `X_MOBILE_DEVICE` header,
+ i.e.: `X_MOBILE_DEVICE: iPhone`
+
+1. Search for a UAProf device. More about UAProf detection can be
+ found [here](http://www.developershome.com/wap/detection/detection.asp?page=profileHeader).
+
+ If a UAProf device is detected, it will have `X_MOBILE_DEVICE: true`
+
+1. Look at the HTTP Accept header to see if the device accepts WAP
+ content. More information about this form of detection is found
+ [here](http://www.developershome.com/wap/detection/detection.asp?page=httpHeaders).
+
+ Any device detected using this method will have `X_MOBILE_DEVICE`
+ set to 'true'.
+
+1. Use a 'catch-all' regex. The current catch-all regex was taken from
+ the [mobile-fu project](http://github.com/brendanlim/mobile-fu)
+
+ Any device detected using this method will have `X_MOBILE_DEVICE: true`
+
+Notes
+=====
+
+If none of the detection methods detected a mobile device, the
+`X_MOBILE_DEVICE` header will be _absent_.
+
+Note that `Rack::MobileDetect::X_HEADER` holds the string
+'X\_MOBILE\_DEVICE' that is inserted into the request headers.
+
+Usage
+=====
+
+ use Rack::MobileDevice
+
+This allows you to do mobile device detection with the defaults.
+
+ use Rack::MobileDevice, :targeted => /SCH-\w*$|[Bb]lack[Bb]erry\w*/
+
+In this usage you can set the value of the regular expression used to
+target particular devices. This regular expression captures Blackberry
+and Samsung SCH-* model phones. For example, if a phone with the
+user-agent: 'BlackBerry9000/4.6.0.167 Profile/MIDP-2.0
+Configuration/CLDC-1.1 VendorID/102' connects, the value of
+`X_MOBILE_DEVICE` will be set to 'BlackBerry9000'
+
+ use Rack::MobileDevice, :catchall => /mydevice/i
+
+This allows you to limit the catchall expression to only the device
+list you choose.
+
+See the unit test source code for more info.
146 lib/mobile-detect.rb
@@ -0,0 +1,146 @@
+# The MIT License
+#
+# Copyright (c) 2009 Tom Alison
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+module Rack
+
+ # Rack::MobileDetect detects mobile devices and adds an
+ # X_MOBILE_DEVICE header to the request is a mobile device is
+ # detected. The strategy for detecting a mobile device is as
+ # follows:
+ #
+ # 1. Search for a 'targeted' mobile device. A targeted mobile device
+ # is a mobile device you may want to provide special content to
+ # because it has advanced capabilities - for example and iPhone or
+ # Android device. Targeted mobile devices are detected via a Regexp
+ # applied against the HTTP User-Agent header.
+ #
+ # By default, the targeted devices are iPhone, Android and iPod. If
+ # a targeted device is detected, the token match from the regular
+ # expression will be the value passed in the X_MOBILE_DEVICE header,
+ # i.e.: X_MOBILE_DEVICE: iPhone
+ #
+ # 2. Search for a UAProf device. More about UAProf detection can be
+ # found here:
+ # http://www.developershome.com/wap/detection/detection.asp?page=profileHeader
+ #
+ # If a UAProf device is detected, the value of X_MOBILE_DEVICE is
+ # simply set to 'true'.
+ #
+ # 3. Look at the HTTP Accept header to see if the device accepts WAP
+ # content. More information about this form of detection is found
+ # here:
+ # http://www.developershome.com/wap/detection/detection.asp?page=httpHeaders
+ #
+ # Any device detected using this method will have X_MOBILE_DEVICE
+ # set to 'true'.
+ #
+ # 4. Use a 'catch-all' regex. The current catch-all regex was taken
+ # from the mobile-fu project. See:
+ # http://github.com/brendanlim/mobile-fu
+ #
+ # Any device detected using this method will have X_MOBILE_DEVICE
+ # set to 'true'.
+ #
+ # If none of the detection methods detected a mobile device, the
+ # X_MOBILE_DEVICE header will be absent.
+ #
+ # Note that Rack::MobileDetect::X_HEADER holds the string
+ # 'X_MOBILE_DEVICE' that is inserted into the request headers.
+ #
+ # Usage:
+ # use Rack::MobileDevice
+ #
+ # This allows you to do mobile device detection with the defaults.
+ #
+ # use Rack::MobileDevice, :targeted => /SCH-\w*$|[Bb]lack[Bb]erry\w*/
+ #
+ # In this usage you can set the value of the regular expression used
+ # to target particular devices. This regular expression captures
+ # Blackberry and Samsung SCH-* model phones. For example, if a phone
+ # with the user-agent: 'BlackBerry9000/4.6.0.167 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/102'
+ # connects, the value of X_MOBILE_DEVICE will be set to 'BlackBerry9000'
+ #
+ # use Rack::MobileDevice, :catchall => /mydevice/i
+ #
+ # This allows you to limit the catchall expression to only the
+ # device list you choose.
+ #
+ # See the unit test source code for more info.
+ #
+ #
+ class MobileDetect
+ X_HEADER = 'X_MOBILE_DEVICE'
+
+ # Users can pass in a :targeted option, which should be a Regexp
+ # specifying which user-agent agent tokens should be specifically
+ # captured and passed along in the X_MOBILE_DEVICE variable.
+ #
+ # The :catchall option allows specifying a Regexp to catch mobile
+ # devices that fall through the other tests.
+ def initialize(app, options = {})
+ @app = app
+
+ # @ua_targeted holds a list of user-agent tokens that are
+ # captured. Captured tokens are passed through in the
+ # environment variable. These are special mobile devices that
+ # may have special rendering capabilities for you to target.
+ @regex_ua_targeted = options[:targeted] || /iphone|android|ipod/i
+
+ # Match mobile content in Accept header:
+ # http://www.developershome.com/wap/detection/detection.asp?page=httpHeaders
+ @regex_accept = /vnd\.wap/i
+
+ # From mobile-fu: http://github.com/brendanlim/mobile-fu
+ @regex_ua_catchall = options[:catchall] ||
+ Regexp.new('palm|palmos|palmsource|iphone|blackberry|nokia|phone|midp|mobi|pda|' +
+ 'wap|java|nokia|hand|symbian|chtml|wml|ericsson|lg|audiovox|motorola|' +
+ 'samsung|sanyo|sharp|telit|tsm|mobile|mini|windows ce|smartphone|' +
+ '240x320|320x320|mobileexplorer|j2me|sgh|portable|sprint|vodafone|' +
+ 'docomo|kddi|softbank|pdxgw|j-phone|astel|minimo|plucker|netfront|' +
+ 'xiino|mot-v|mot-e|portalmmm|sagem|sie-s|sie-m|android|ipod', true)
+ end
+
+ # Because the web app may be multithreaded, this method must
+ # create new Regexp instances to ensure thread safety.
+ def call(env)
+ device = nil
+ user_agent = env.fetch('HTTP_USER_AGENT', '')
+
+ # First check for targeted devices and store the device token
+ device = Regexp.new(@regex_ua_targeted).match(user_agent)
+
+ # Fall-back on UAProf detection
+ # http://www.developershome.com/wap/detection/detection.asp?page=profileHeader
+ device ||= env.keys.detect { |k| k.start_with?('HTTP_') && k.end_with?('_PROFILE') } != nil
+
+ # Fall back to Accept header detection
+ device ||= Regexp.new(@regex_accept).match(env.fetch('HTTP_ACCEPT','')) != nil
+
+ # Fall back on catch-all User-Agent regex
+ device ||= Regexp.new(@regex_ua_catchall).match(user_agent) != nil
+
+ env[X_HEADER] = device.to_s if device
+
+ @app.call(env)
+ end
+ end
+end
197 test/test_mobile_detect.rb
@@ -0,0 +1,197 @@
+require 'rubygems'
+require 'test/unit'
+require 'shoulda'
+
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'mobile-detect'
+
+class TestMobileDevice < Test::Unit::TestCase
+
+ context "An app with mobile-device defaults" do
+ setup do
+ @app = test_app
+ @rack = Rack::MobileDetect.new(@app)
+ end
+
+ should "not detect a non-mobile device" do
+ env = test_env
+ @rack.call(env)
+ assert !env.key?(x_mobile)
+ end
+
+ should "detect all default targeted devices" do
+ env = test_env({ 'HTTP_USER_AGENT' => ipod })
+ @rack.call(env)
+ assert_equal 'iPod', env[x_mobile]
+
+ env = test_env({ 'HTTP_USER_AGENT' => iphone })
+ @rack.call(env)
+ assert_equal 'iPhone', env[x_mobile]
+
+ env = test_env({ 'HTTP_USER_AGENT' => android })
+ @rack.call(env)
+ assert_equal 'Android', env[x_mobile]
+ end
+
+ should "detect UAProf device" do
+ env = test_env({ 'HTTP_X_WAP_PROFILE' =>
+ '"http://www.blackberry.net/go/mobile/profiles/uaprof/9000_80211g/4.6.0.rdf"' })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+
+ env = test_env({ 'HTTP_PROFILE' =>
+ 'http://www.blackberry.net/go/mobile/profiles/uaprof/9000_80211g/4.6.0.rdf' })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+
+ # See http://www.developershome.com/wap/detection/detection.asp?page=uaprof
+ env = test_env({ 'HTTP_80_PROFILE' =>
+ 'http://wap.sonyericsson.com/UAprof/T68R502.xml' })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+ end
+
+ should "not detect spurious profile header match" do
+ env = test_env({ 'HTTP_X_PROFILE_FOO' => 'bar' })
+ @rack.call(env)
+ assert !env.key?(x_mobile)
+ end
+
+ should "detect wap in Accept header" do
+ env = test_env({ 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/vnd.wap.xhtml+xml,*/*;q=0.5' })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+
+ env = test_env({ 'HTTP_ACCEPT' => 'application/vnd.wap.wmlscriptc;q=0.7,text/vnd.wap.wml;q=0.7,*/*;q=0.5' })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+ end
+
+ should "detect additional devices in catchall" do
+ env = test_env({ 'HTTP_USER_AGENT' => blackberry })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+
+ env = test_env({ 'HTTP_USER_AGENT' => samsung })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+
+ env = test_env({ 'HTTP_USER_AGENT' => 'opera' })
+ @rack.call(env)
+ assert !env.key?(x_mobile)
+ end
+ end
+
+ context "An app with a custom targeted option" do
+ setup do
+ @app = test_app
+ # Target Samsung SCH and Blackberries. Note case-sensitivity.
+ @rack = Rack::MobileDetect.new(@app, :targeted => /SCH-\w*$|[Bb]lack[Bb]erry\w*/)
+ end
+
+ should "capture the targeted token" do
+ env = test_env({ 'HTTP_USER_AGENT' => samsung })
+ @rack.call(env)
+ assert_equal 'SCH-U960', env[x_mobile]
+
+ env = test_env({ 'HTTP_USER_AGENT' => "Samsung SCH-I760" })
+ @rack.call(env)
+ assert_equal 'SCH-I760', env[x_mobile]
+
+ env = test_env({ 'HTTP_USER_AGENT' => blackberry })
+ @rack.call(env)
+ assert_equal 'BlackBerry9000', env[x_mobile]
+
+ # An iPhone will be detected, but the token won't be captured
+ env = test_env({ 'HTTP_USER_AGENT' => iphone })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+ end
+ end
+
+ context "An app with a custom catchall option" do
+ setup do
+ @app = test_app
+ # Custom catchall regex
+ @rack = Rack::MobileDetect.new(@app, :catchall => /mysupermobiledevice/i)
+ end
+
+ should "catch only the specified devices" do
+ env = test_env({ 'HTTP_USER_AGENT' => "MySuperMobileDevice v1.0" })
+ @rack.call(env)
+ assert_equal "true", env[x_mobile]
+
+ env = test_env({ 'HTTP_USER_AGENT' => samsung })
+ @rack.call(env)
+ assert !env.key?(x_mobile)
+ end
+ end
+
+ # Expected x_header
+ def x_mobile
+ Rack::MobileDetect::X_HEADER
+ end
+
+ # User agents for testing
+ def ipod
+ 'Mozilla/5.0 (iPod; U; CPU iPhone OS 2_2 like Mac OS X; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5G77 Safari/525.20'
+ end
+
+ def iphone
+ 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7C144 Safari/528.16'
+ end
+
+ def android
+ 'Mozilla/5.0 (Linux; U; Android 2.0; ld-us; sdk Build/ECLAIR) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17'
+ end
+
+ def blackberry
+ 'BlackBerry9000/4.6.0.167 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/102'
+ end
+
+ def samsung
+ 'Mozilla/4.0 (compatible; MSIE 6.0; BREW 3.1.5; en )/800x480 Samsung SCH-U960'
+ end
+
+ # Our test web app. Doesn't do anything.
+ def test_app()
+ Class.new { def call(app); true; end }.new
+ end
+
+ # Test environment variables
+ def test_env(overwrite = {})
+ {
+ 'GATEWAY_INTERFACE'=> 'CGI/1.2',
+ 'HTTP_ACCEPT'=> 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'HTTP_ACCEPT_CHARSET'=> 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'HTTP_ACCEPT_ENCODING'=> 'gzip,deflate',
+ 'HTTP_ACCEPT_LANGUAGE'=> 'en-us,en;q=0.5',
+ 'HTTP_CONNECTION'=> 'keep-alive',
+ 'HTTP_HOST'=> 'localhost:4567',
+ 'HTTP_KEEP_ALIVE'=> 300,
+ 'HTTP_USER_AGENT'=> 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.3) Gecko/20090920 Firefox/3.5.3 (Swiftfox)',
+ 'HTTP_VERSION'=> 'HTTP/1.1',
+ 'PATH_INFO'=> '/',
+ 'QUERY_STRING'=> '',
+ 'REMOTE_ADDR'=> '127.0.0.1',
+ 'REQUEST_METHOD'=> 'GET',
+ 'REQUEST_PATH'=> '/',
+ 'REQUEST_URI'=> '/',
+ 'SCRIPT_NAME'=> '',
+ 'SERVER_NAME'=> 'localhost',
+ 'SERVER_PORT'=> '4567',
+ 'SERVER_PROTOCOL'=> 'HTTP/1.1',
+ 'SERVER_SOFTWARE'=> 'Mongrel 1.1.5',
+ 'rack.multiprocess'=> false,
+ 'rack.multithread'=> true,
+ 'rack.request.form_hash'=> '',
+ 'rack.request.form_vars'=> '',
+ 'rack.request.query_hash'=> '',
+ 'rack.request.query_string'=> '',
+ 'rack.run_once'=> false,
+ 'rack.url_scheme'=> 'http',
+ 'rack.version'=> '1: 0'
+ }.merge(overwrite)
+ end
+end
20 util/echo_env.rb
@@ -0,0 +1,20 @@
+require 'rubygems'
+require 'sinatra'
+
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'mobile-detect'
+
+use Rack::MobileDetect
+
+# Very simple sinatra app that allows debugging of the headers with
+# Rack::MobileDetect. Also useful for looking at various mobile phone
+# headers.
+get '/' do
+ content_type 'text/plain'
+ env_string = env.sort.map{ |v| v.join(': ') }.join("\n") + "\n"
+ # Print to log if debug is passed, i.e.:
+ # http://localhost:4567/?debug
+ puts env_string if params.key?('debug')
+ env_string
+end

0 comments on commit d93168b

Please sign in to comment.