ExtAuth – A CakePHP 2.x Component for easy federated login via Google, Twitter, Facebook, and more

I’ve released a CakePHP component that I’ve been working on on GitHub – comments, criticisms welcome. The component handles OAuth (1.0, 1.0a and 2.0) authentication, and is designed to allow the user to very rapidly get federated login up and running, across multiple providers, with as little fuss as possible.

ExtAuth does not provide a User Model or User Controller. It is provided as a Component that is to be
integrated into your own (or another plugin’s) User Controller. An example of how to do this is below.

Installation

ExtAuth is installed the same way as any other CakePHP 2.x plugin. Simply copy the ExtAuth folder to App/Plugin.

Configuration

  1. Make sure that the plugin gets loaded by CakePHP. To do this, place the line
    CakePlugin::load('ExtAuth');

    at the bottom of your Config/bootstrap.php file.

  2. For each of the login providers that you wish to use, you need to create an app/project, set the callback URLs, obtain an app key
    and an app secret, and tell ExtAuth what these are. Here’s how:

    Google

    1. Browse to https://code.google.com/apis/console.
    2. Create a project, if you have not already.
    3. Click on API Access
    4. Create a client ID. You need to set the Redirect URI to http://example.com/auth_callback/google
      (replace example.com with your domain name. You can’t use an invalid TLD, such as example.dev,
      for testing – Google does not allow this. you’ll need to use dev.example.com instead, as well
      as your example.com live domain.) The path of the callback can be changed in the ExtAuth component’s settings.
    5. Create two lines in your app’s Config/core.php file (or elsewhere if you have another location for app settings)
      similar to the following:

      		Configure::write('ExtAuth.Provider.Google.key', '123456789012.apps.googleusercontent.com');
      		Configure::write('ExtAuth.Provider.Google.secret', 'blahblahblahblahblahblah');
      		

      Replace the config values with the Client ID and Client secret for your project in the Google API console.

    Twitter

    1. Browse to https://dev.twitter.com/apps
    2. Create an application if you have not got one already. The Callback URL needs to be http://example.com/auth_callback/twitter.
      The path of the callback can be changed in the ExtAuth component’s settings if needs be.
    3. Create two lines in your app’s Config/core.php file (or elsewhere if you have another location for app settings)
      similar to the following:

      		Configure::write('ExtAuth.Provider.Twitter.key', 'blahblahblah');
      		Configure::write('ExtAuth.Provider.Twitter.secret', 'blahblahblahblahblahblah');
      		

      Replace the config values with the Consumer key and Consumer secret for your app on the Twitter apps Details tab.

    4. Make sure “Sign In With Twitter” is ticked in the settings for your app on Twitter.

    Facebook

    1. Browse to https://developers.facebook.com/apps.
    2. Create an app, if you have not done so already. Set the “App Domain” as the domain name of your CakePHP site.
    3. Click on “Website with Facebook Login”. set the site URL as the URL of your CakePHP site.
    4. Create two lines in your app’s Config/core.php file (or elsewhere if you have another location for app settings)
      similar to the following:

      		Configure::write('ExtAuth.Provider.Facebook.key', '431235136634241414');
      		Configure::write('ExtAuth.Provider.Facebook.secret', '12344g4f3241e4d2144');
      		

      Replace the config values with the App ID and App Secret for your app in Facebook.

Usage

Firstly, ensure that your User Controller is loading the ExtAuth component, as well as CakePHP’s Auth component:

public $components = array('ExtAuth', 'Auth', 'Session');

You will need, at minimum, two actions in your User Controller. These will initiate the authentication and handle a callback from
the provider. Something like this:

public function auth_login($provider) {
	$result = $this->ExtAuth->login($provider);
	if ($result['success']) {

		$this->redirect($result['redirectURL']);

	} else {
		$this->Session->setFlash($result['message']);
		$this->redirect($this->Auth->loginAction);
	}
}

public function auth_callback($provider) {
	$result = $this->ExtAuth->loginCallback($provider);
	if ($result['success']) {

		$this->__successfulExtAuth($result['profile'], $result['accessToken']);

	} else {
		$this->Session->setFlash($result['message']);
		$this->redirect($this->Auth->loginAction);
	}
}

You will also need to create two routes in Config/routes.php, similar to the following:

Router::connect('/auth_login/*', array( 'controller' => 'users', 'action' => 'auth_login'));
Router::connect('/auth_callback/*', array( 'controller' => 'users', 'action' => 'auth_callback'));

That’s it. I’ll leave it to you to implement the __successfulExtAuth function, but, you might want something similar to this:

private function __successfulExtAuth($incomingProfile, $accessToken) {

	// search for profile
	$this->SocialProfile->recursive = -1;
	$existingProfile = $this->SocialProfile->find('first', array(
		'conditions' => array('oid' => $incomingProfile['oid'])
	));

	if ($existingProfile) {

		// Existing profile? log the associated user in.
		$user = $this->User->find('first', array(
			'conditions' => array('id' => $existingProfile['SocialProfile']['user_id'])
		));

		$this->__doAuthLogin($user);
	} else {

		// New profile.
		if ($this->Auth->loggedIn()) {

			// user logged in already, attach profile to logged in user.

			// create social profile linked to current user
			$incomingProfile['user_id'] = $this->Auth->user('id');
			$this->SocialProfile->save($incomingProfile);
			$this->Session->setFlash('Your ' . $incomingProfile['provider'] . ' account has been linked.');
			$this->redirect($this->Auth->loginRedirect);

		} else {

			// no-one logged in, must be a registration.
			unset($incomingProfile['id']);
			$user = $this->User->register(array('User' => $incomingProfile));

			// create social profile linked to new user
			$incomingProfile['user_id'] = $user['User']['id'];
			$incomingProfile['last_login'] = date('Y-m-d h:i:s');
			$incomingProfile['access_token'] = serialize($accessToken);
			$this->SocialProfile->save($incomingProfile);

			// populate user detail fields that can be extracted
			// from social profile
			$profileData = array_intersect_key(
				$incomingProfile,
				array_flip(array(
					'email',
					'given_name',
					'family_name',
					'picture',
					'gender',
					'locale',
					'birthday',
					'raw'
				))
			);

			$this->User->setupDetail();
			$this->User->UserDetail->saveSection(
				$user['User']['id'],
				array('UserDetail' => $profileData),
				'User'
			);

			// log in
			$this->__doAuthLogin($user);
		}
	}
}

private function __doAuthLogin($user) {
	if ($this->Auth->login($user['User'])) {
		$user['last_login'] = date('Y-m-d H:i:s');
		$this->User->save(array('User' => $user));

		$this->Session->setFlash(sprintf(__d('users', '%s you have successfully logged in'), $this->Auth->user('username')));
		$this->redirect($this->Auth->loginRedirect);
	}
}

Javascript function to convert a string in dot and/or array notation into a reference

I needed to convert a string into an object reference. Over on StackOverflow, ninjagecko had created a beautiful function to resolve dotted string references, but I needed to handle array style references as well as dotted ones. Here is my resulting function, I hope it can be of use to someone else!

string_to_ref = function (object, reference) {
    function arr_deref(o, ref, i) {
        return !ref ? o : (o[ref.slice(0, i ? -1 : ref.length)]);
    }
    function dot_deref(o, ref) {
        return !ref ? o : ref.split('[').reduce(arr_deref, o);
    }
    return reference.split('.').reduce(dot_deref, object);
};

now I can use my typeahead tag directive for AngularJS, and pass through the model in an attribute, even if it is a reference deep into an object, like so:


Before you ask, no, it can’t handle nested variables, such as something like this:


This is not possible without using evil eval, since it is not possible to get a reference to the closure containing current_persona. I suppose I could make a version that would work with references to global variables only, but you shouldn’t be polluting the global namespace. Besides, the same can be acheived by a bit of string concatenation and AngularJS’ {{}} operators.

I’ve created a JSFiddle so you can, well, fiddle with the JS. It contains some unit tests using QUnit so that you can check it works.

Magento – Set Dropdown Attribute Value of a Product, Add Option if not Present

A quick little function for PHP scripts. Dropdown attributes have a limited set of allowed values. Say you have products that you wish the customer to be able to filter by Manufacturer on the front-end: the manufacturer attribute must be a drop-down in order for it to be filterable. If your products are added from an importer script, the script needs to be able to add a missing Manufacturer to the options of the attribute, if it is not already on the list. This function handles that transparently. If the value exists, it is set. If it does not exist, it is created and then set.

function setOrAddOptionAttribute($product, $arg_attribute, $arg_value) {
	$attribute_model = Mage::getModel('eav/entity_attribute');
	$attribute_options_model = Mage::getModel('eav/entity_attribute_source_table');

	$attribute_code = $attribute_model->getIdByCode('catalog_product', $arg_attribute);
	$attribute = $attribute_model->load($attribute_code);

	$attribute_options_model->setAttribute($attribute);
	$options = $attribute_options_model->getAllOptions(false);

	// determine if this option exists
	$value_exists = false;
	foreach($options as $option) {
		if ($option['label'] == $arg_value) {
			$value_exists = true;
			break;
		}
	}

	// if this option does not exist, add it.
	if (!$value_exists) {
		$attribute->setData('option', array(
			'value' => array(
				'option' => array($arg_value,$arg_value)
			)
		));
		$attribute->save();
	}

	$product->setData($arg_attribute, $arg_value);
}

An example:

setOrAddOptionAttribute($product, 'manufacturer', 'Samsung');

Sophos #DECODEME 2012

Sophos have created a puzzle to coincide with this year’s AusCERT security conference. Solve the puzzle shown here, to win a prize of a full-auto NERF gun. The puzzle is printed on a T-shirt, an image of which is shown at the Naked Security blog on the link above. The puzzle has not been published fully yet, but we can start a bit early based on the image… Each of the symbols (ignore the “GET SMART” ASCII art pipes/slashes/underscores, and the text underneath) can be found on the number keys on a US Keyboard, by pressing shift. The symbols * and ( do not appear – and hence if we convert each symbol into its corresponding number, the numbers 8 and 9 do not appear, suggesting the code is using an octal-based encoding. if we group the digits in threes, and convert each of the 3-digit groups to the corresponding ASCII character, we get something interesting. I’ve created This Pageto help people decode this first stage. Input the symbols, and see what it decodes to. The symbols are:

     !@#!^$!$!!$&!$%)$))$#)^@)$)!@%!@@
     !!$)&@)$)!%)!^$!^$!^))&@)%&)%&!^&
     !^&!_____ ______ _______ _____^&)
     %^!/ ____|  ____|__   __/ ____|^#
     !%| |  __| |__     | | | |__&!^)!
     %)| | |_ |  __|    | |  \___ \!%&
     !^| |__| | |____   | |  ____| |#)
     %^!\_____|______|  |_| |_____/$#!
     %&!%%)%&!$!!%^!&@)%&!^%!^#!%#!$%!
     &!__  __          _____ _______!$
     @|  \/  |   /\   |  __ \__   __|!
     %| \  / |  /  \  | |__| | | |&!$!
     !| |\/| | / /\ \ |  _  /  | |^@!$
     $| |  | |/ ____ \| | \ \  | |)%^!
     %|_|  |_/_/    \_\_|  \_\ |_|)!^$
     !%%!%$)$)!^&!%!!^$!%))$)!%%!$!!$&
     !%!!$#)$)!%^!%&)%^)$))^!)^))^))&) 

       D   E   C   O   D   E   M   E
       2012 geek fashion from sophos

But this clearly is incomplete if you input it into my decode page. In fact, Sophos’ Paul Ducklin has removed some of the symbols prior to the start of the competition. The full text of the first part of the puzzle is now available. I’m guessing that it should translate to this link, but it is not live yet, or just plain wrong! In the image, inside the symbols, is the phrase “GET SMART” – is this a reference to a HTTP GET request? combined with the phrase “with mag”, at the end of the decryption, this could potentially give this link. But, at least at the moment, this also 404′s. Anyone else got any ideas?

15/05/12 08:48 UPDATE: Looks like I was correct about the URL! I have also updated the symbols in the box above to contain the full list. For part two, this is just a variation on The Birthday Paradox. Instead of looking for birthday collisions we are looking for card ones, so simply use the technique you would use for the birthday paradox but with 52 cards instead of 365 days, like so: (52! – 14!) / 52^13 Scotty

 

15/05/12 09:28 UPDATE: I’ve submitted my final solution – looks like I’ve come second or third… https://twitter.com/#!/duckblog/status/202314487888478209

 

15/05/12 09:33 UPDATE: I came third, by less than 60s :o) https://twitter.com/#!/duckblog/status/202315878438666240 I should have skipped breakfast, I got the full text 20 minutes after it was released!

Magento SQL query to set a base image for any products that do not have one set.

I had to do some work on a Magento site today, where the site admin had added images to all the products, but had not set any of them as base, small, or thumb images – so none of the listings were showing an image on the front end. I googled for a SQL query to do the job, and found none – so I knocked this one up – maybe someone else will find it useful.

The queries do not affect any products that already have the respective values already set.

Base Images:

INSERT INTO catalog_product_entity_varchar (entity_type_id, attribute_id, store_id, entity_id, value) SELECT 4, 79, 0, cpev.entity_id, cpemg.value FROM catalog_product_entity_varchar AS cpev INNER JOIN catalog_product_entity_media_gallery AS cpemg ON cpev.entity_id = cpemg.entity_id WHERE cpev.entity_id NOT IN (SELECT entity_id FROM catalog_product_entity_varchar WHERE entity_type_id = 4 AND attribute_id = 79 AND value IS NOT NULL AND value !=  'no_selection')

Small Images:

INSERT INTO catalog_product_entity_varchar (entity_type_id, attribute_id, store_id, entity_id, value) SELECT 4, 80, 0, cpev.entity_id, cpemg.value FROM catalog_product_entity_varchar AS cpev INNER JOIN catalog_product_entity_media_gallery AS cpemg ON cpev.entity_id = cpemg.entity_id WHERE cpev.entity_id NOT IN (SELECT entity_id FROM catalog_product_entity_varchar WHERE entity_type_id = 4 AND attribute_id = 80 AND value IS NOT NULL AND value !=  'no_selection')

Thumb Images:

INSERT INTO catalog_product_entity_varchar (entity_type_id, attribute_id, store_id, entity_id, value) SELECT 4, 81, 0, cpev.entity_id, cpemg.value FROM catalog_product_entity_varchar AS cpev INNER JOIN catalog_product_entity_media_gallery AS cpemg ON cpev.entity_id = cpemg.entity_id WHERE cpev.entity_id NOT IN (SELECT entity_id FROM catalog_product_entity_varchar WHERE entity_type_id = 4 AND attribute_id = 81 AND value IS NOT NULL AND value !=  'no_selection')

Hopefully this can save someone else a bit of time!

yepnope and jQuery – fix for $(document).ready calls

I recently added the fantastic yepnope.js to a site in order to give it a bit of a speedup. The site already had jQuery, including a few calls to $(document).ready() across a few different files. As anybody who has tried this knows, this breaks your $(document).ready() calls. jQuery is not loaded at the point that the browser tries to execute the document ready call, so the browser gives an error, complaining that $ is undefined. Here is my solution.

First, add this in your <head>, before yepnope and anything that could use jQuery. This sets up a placeholder that stores any calls to $(document).ready() in an array called docready.

var docready=[],$=function(o){function r(fn){docready.push(fn);}if(typeof o === 'function') r(o);return{ready: r}};

Secondly, in your yepnope call to load jQuery, change the “complete” call like so:

yepnope({
	load: '//ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js',
	callback: function (url, result, key) {
		if (!window.jQuery) yepnope('/js/jquery.min.js');
	},
	complete: function() {
		$ = jQuery;
		jQuery(document).ready(function() {
			for(n in docready) docready[n]();
		});
	}
});

So now, once jQuery has loaded, $ is reassigned from our placeholder to jQuery, and the stored $(document).ready() calls are iterated through and executed once ready fires. Sweet!

Edit: Updated with new, more concise, complete callback.

Edit 2: Updated to handle $(function() {}) style invocations, thanks to Chris Jones

OpenSearch – A quick tip

I’ve learnt some great new skills today involving OpenSearch, and browser search plug-ins. Not enough time to put it all into an article right now, but I will work on one over the next few days.

In the meantime, a quick tip – something that may save someone a bit of time.

When linking to an OpenSearch description document to enable auto-discovery, an HTML link tag to the XML OSDD is used. The type property is set to “application/opensearchdescription+xml”, as per the OpenSearch spec. By default, however, as the OSDD file generally has a .xml suffix, Apache will serve the image up as text/xml. This can cause the browser to not accept the OSDD. The solution, if you are using .htaccess files, is a simple addition to the .htaccess file. Replace opensearch.xml with the name of your OSDD file:

<Files opensearch.xml>
    ForceType application/opensearchdescription+xml
<Files>

Simple, I know, but it may cause a bit of head-scratching. More to come.

Email Validation – via Polymorphic PHP / JS hybrid script, internationalized CC TLD ready

I was looking around for a RegEx to match an email address last night and headed straight to, what must be, the RegEx Mecca – http://www.regular-expressions.info . The page discussing email validation is very comprehensive and informative. The section that got my mind going was under the heading “Trade-Offs in Validating Email Addresses”; in particular the lengthy RegEx and it’s associated critique:

^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|asia|jobs|museum)$ Analyze this regular expression with RegexBuddy could be used to allow any two-letter country code top level domain, and only specific generic top level domains. By the time you read this, the list might already be out of date. If you use this regular expression, I recommend you store it in a global constant in your application, so you only have to update it in one place. You could list all country codes in the same manner, even though there are almost 200 of them.

So I thought to myself, what if you could somehow automatically update the list of valid TLD’s, generate the RegEx from that, and then just use a simple function to test the RegEx? JS would be out of the window for the auto-update, unless you wanted the client to have an extra HTTP request for every visit to a page with your function. But how about a PHP script that could update the TLD list, direct from IANA, say, once per day, and cache this request. Combine the JS with the PHP by having the PHP script generate the JS programatically, by setting the script’s header’s content type attribute as  application/x-javascript. And for fun, and because I never get to do any self-modifying code, have the PHP script store its updates from IANA in itself – so that it would only need to refresh its TLD list when it checks it’s own file modification time and finds itself to be unmodified for over a day.

Well, here is the result (download link at bottom of post):

< ?php
/*****************************************************
* valEmail.js.php
* 
* @author Scott Donnelly 
* @version 1.0
* Released under the LGPL V3 license.
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*
* See http://scott.donnel.ly/archive/140 for info
* thanks to http://www.regular-expressions.info
******************************************************/

// TLDs last updated: Never
$TLDs = Array();

// update list of top-level domains from IANA if current list is too old.
if (empty($TLDs) || filemtime('./valEmail.js.php') < time() - 86400) {
    
    // get the file from IANA; gracefully fall back to current data on fail
    if ($iana_TLD_file = file_get_contents('http://data.iana.org/TLD/tlds-alpha-by-domain.txt')) {
    
        $TLDs = Array();
        $iana_TLD_file = explode("\n", $iana_TLD_file);
        
        // parse all TLD's into TLD array - ignore comments/too short lines
        foreach($iana_TLD_file as $line)
            if (strlen(trim($line)) > 1 && $line[0] != "#")
                array_push($TLDs, trim($line));
                
        // self-modify to update cached TLD Array
        if ($this_file = file_get_contents('valEmail.js.php')) {
        
            // rip the document block at the start of the file.
            $start_of_file = substr($this_file, 0, strpos($this_file, "// TLDs last updated"));
            
            // synthesize the first 3 lines of the file after the docblock...
            $start_of_file .= "// TLDs last updated: "
                . date('D/n/Y G:i:s T') . "\n"
                . '$TLDs = Array(';
            
            $first = true;
            foreach($TLDs as $TLD) {
                if ($first) $first = false; else $start_of_file .= ", ";
                $start_of_file .= "'$TLD'";
            }
            $start_of_file .= ");\n";
            
            // ... append the rest of the file as per current file...
            $this_file = $start_of_file . substr($this_file, strpos($this_file, ");\n") + 3);
            
            // .. and write it out to the file system.
            if ($fp = fopen('./valEmail.js.php', 'wt')) {
                fwrite($fp, $this_file);
                fclose($fp);
            }
        }
    }
}

// output the email check function as a JS file.
header('Content-type: application/x-javascript');

echo "function valEmail(email) {   
        emailRE = new RegExp('[a-z0-9!#$%\&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%\&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+(?:";

$first = true;
foreach($TLDs as $TLD) {
    if ($first) $first = false; else echo "|";    
    echo(strtolower($TLD));
}

echo ")$')\n\treturn emailRE.test(email);\n}\n";
?>

Executing the code the first time triggers the script to modify itself, rewriting lines 13 and 14 to:

// TLDs last updated: Mon/9/2010 21:43:00 BST
$TLDs = Array('AC', 'AD', 'AE', 'AERO', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ', 'AR', 'ARPA', 'AS', 'ASIA', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BIZ', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CAT', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'COM', 'COOP', 'CR', 'CU', 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EDU', 'EE', 'EG', 'ER', 'ES', 'ET', 'EU', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GOV', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'INFO', 'INT', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JOBS', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MG', 'MH', 'MIL', 'MK', 'ML', 'MM', 'MN', 'MO', 'MOBI', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MUSEUM', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NAME', 'NC', 'NE', 'NET', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'ORG', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PRO', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SU', 'SV', 'SY', 'SZ', 'TC', 'TD', 'TEL', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TP', 'TR', 'TRAVEL', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UK', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'XN--0ZWM56D', 'XN--11B5BS3A9AJ6G', 'XN--80AKHBYKNJ4F', 'XN--9T4B11YI5A', 'XN--DEBA0AD', 'XN--FIQS8S', 'XN--FIQZ9S', 'XN--FZC2C9E2C', 'XN--G6W251D', 'XN--HGBK6AJ7F53BBA', 'XN--HLCJ6AYA9ESC7A', 'XN--J6W193G', 'XN--JXALPDLP', 'XN--KGBECHTV', 'XN--KPRW13D', 'XN--KPRY57D', 'XN--MGBAAM7A8H', 'XN--MGBAYH7GPA', 'XN--MGBERP4A5D4AR', 'XN--O3CW4H', 'XN--P1AI', 'XN--PGBS0DH', 'XN--WGBH1C', 'XN--XKC2AL3HYE2A', 'XN--YGBI2AMMX', 'XN--ZCKZAH', 'YE', 'YT', 'ZA', 'ZM', 'ZW');

Anyway, the script produces (at the time of writing!) the following JavaScript function:

function valEmail(email) {	
		emailRE = new RegExp('[a-z0-9!#$%\&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%\&\'*+/=?^_`{|}~-]+)*@
(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+?:ac|ad|ae|aero|af|ag|ai|al|am|an|ao|aq|ar|arpa|as|asia|at|au|
aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|biz|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cat|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|
com|coop|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|
gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|info|int|io|iq|ir|is|it|je|jm|jo|jobs|jp|ke|
kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mo|mobi|
mp|mq|mr|ms|mt|mu|museum|mv|mw|mx|my|mz|na|name|nc|ne|net|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|
pf|pg|ph|pk|pl|pm|pn|pr|pro|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|
sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|travel|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|
xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--fiqs8s|xn--fiqz9s|
xn--fzc2c9e2c|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--j6w193g|xn--jxalpdlp|xn--kgbechtv|
xn--kprw13d|xn--kpry57d|xn--mgbaam7a8h|xn--mgbayh7gpa|xn--mgberp4a5d4ar|xn--o3cw4h|xn--p1ai|
xn--pgbs0dh|xn--wgbh1c|xn--xkc2al3hye2a|xn--ygbi2ammx|xn--zckzah|ye|yt|za|zm|zw)$')
	return emailRE.test(email);
}

Not pretty, but “fire-and-forget” – you will never need to look at this ugly JS as it will never need editing (These sound like famous last words… no more than 640k anyone??! :-) )

Anyways, you can give it a try here:


You can downoad the zipped valEmail.js.php file using this link.