Stop validating e-mail addresses

Posted on October 31st, 2008 in Rants | 2 Comments »

Because you’re doing it wrong. At least, that’s what I’ve discovered to be the case with Borders.com, JustFlowers.com, and a number of other sites.

My personal e-mail address has a .name top-level domain. Dot-name, of course, being one of the 280 (at present) valid TLDs. Your rinky-dink regular expression that checks (com|net|org|gov|mil) does not cut it.

This morning I tried to order a book from Borders. I couldn’t. They didn’t like my e-mail address. I also tried to change my password. Couldn’t.

Ultimately, I had to change my e-mail address in order to do anything. Now all of my personal e-mail goes to one address, and all the Borders mail goes to another that I use for technical mailing lists.

Look, e-mail addresses are complicated. More complicated than you think. See Phil Haack’s enlightening blog post on the subject if you don’t believe me.

Did you ever consider why you are validating e-mail addresses in the first place? It’s in the customer’s best interest for an order confirmation e-mail to get to their inbox. Why do you put two text fields to confirm an address? It’s to help prevent the user from making dumb mistakes, right? The fact is there’s no need for rigid validation—either the e-mail gets there or it doesn’t.

If you must validate, do this instead: .+@.+. That’s guaranteed to be future-proof, and people like me won’t write you ticked-off e-mails telling you to fix it. ;-)

PHP gets lambda methods, closures

Posted on September 26th, 2008 in Code, PHP | 1 Comment »

Not one month after I wrote about the future of PHP (June 2008), I was quite happily proven wrong.

For my part, I’d like to see first-class functions and closures included in the language. [...]

But none of that will happen, because PHP is a language in decline. Not a decline in usage—it will only continue to expand its reach—but in the addition of innovative features from other languages. There will be no need to evolve; most of the agitators for change will have moved on.
Me

It’s always been in my nature to own up to it when I’m proven wrong, so consider this my mea culpa. PHP has always been a klugey language, borrowing from other languages and implementing those ideas in somewhat endearingly clunky ways. I naively believed that that dynamism was coming to an end, but as you can see from the link above, that’s demonstrably not the case. Despite that, I still think there is a slow but steady “brain drain” from the ranks of the top tier of PHP developers—I’ve seen it first hand in the last few years and the overall trend should make PHP developers at least a little uncomfortable.

Anyway, the closure implementation coming in PHP 5.3 is, like namespaces, a little clunky.

function replace_in_array($search, $replacement, $array) {
    $map = function ($text) use ($search, $replacement) {
        if (strpos($text, $search) > 50) {
            return str_replace($search, $replacement, $text);
        }
        return $text;
    };
    return array_map ($map, $array);
}

Yeah, you have to manually link the variables to make them available to the closure. Not ideal, but neither is having to manually specify your scope in JavaScript (via Function.apply()).

There are a few of other differences in PHP 5.3’s implementation of closures and other languages:

  • First, like other functions, they have access to the global scope with the global keyword. Do yourself a great big favor and just avoid doing that.
  • Second, you can choose which variables are linked by reference and which are not.
  • Finally, you can declare an anonymous function static if it’s declared in a class but doesn’t use an instance of that class for anything. If you have a large object, this would prevent the closure from retaining a reference to that instance (and therefore, its memory footprint) after it has outlived its usefulness. This will probably be the least understood aspect of PHP closures for most developers.

Currying is now possible as well. Ryan Timmons wasted no time in writing a method for doing just that:

function curry($function, $argument) {
    return function() use ($function, $argument) {
        $arguments = func_get_args();
        array_unshift($arguments, $argument);
        return call_user_func_array($function, $arguments);
    };
}

Between this, namespaces, late static binding, and a bundled packaging method (ext/phar), the next version of PHP is looking more like a major release instead of a minor one.

PHP 5.3 is scheduled for final release in October [update: it's been pushed back to the end of Q1 2009].

Zend_Paginator

Posted on September 3rd, 2008 in Code, Frameworks, PHP, Zend Framework | 5 Comments »

On and off for the last two or three months, Jurriën Stutterheim and I wrote Zend_Paginator, the pagination component for Zend Framework. Yesterday it was officially released as part of Zend Framework 1.6.

This was our first contribution to the framework, and was very much a collaborative working relationship. I first created a proposal in about half an hour on Christmas Eve 2007 for how I thought a pagination component should work after being dissatisfied with existing solutions. It contained some good ideas about flexibility, but on its own it was half an idea, at best. So I promptly forgot about it.

Then Jurriën came along and created a proposal based on his Zym_Paginate component, which was part of Zym Framework, itself an extension of Zend Framework.

At the urging of Zend, we merged our proposals. We refined our ideas, incorporating aspects from each of our proposals in addition to brand new thoughts. Then I wrote a new component from scratch based in part on Jurriën’s existing work. He wrote the excellent unit tests (with 100% code coverage, not always easy to achieve!), and I wrote the DocBook documentation. I also can’t overstate the positive impact the community had, particularly from Bryce Lohr who prompted us to decouple and reorganize major portions of the component.

Anyway, I’m very happy with how it turned out. There are still a few issues, most of them minor—and because of the component’s flexible design, there are almost always workarounds. They should be fixed in time for 1.6.1.

Here’s some praise for the component:

Upcoming Zend_Paginator in 1.6 is absolutely brilliant. Wasted time be gone.
Joakim Nygård

The new Zend Framework 1.6 release candidate includes a Zend_Paginator class, which is an excellent thing to have around because I know I’ve re-invented that wheel on every site I’ve developed. My only criticism of the new Zend_Paginator is it offers a daunting amount of possibilities.
Chris Beer

This really is a beautifully crafted component, very elegant, very classy.
—David Mintz, via e-mail

I’ve seen dozens of different paginators in the last couple of years, and to be honest, this one is by far the best. You got everything right! I can’t wait for this component to hit the incubator.

It’s great, well done guys.
Federico Cargnelutti

ORM comes to the iPhone: SQLite Persistent Objects

Posted on August 30th, 2008 in Frameworks, Mobile, iPhone | 4 Comments »

Although I cut my “programming teeth” on functional database APIs, I happily gave them up years ago in favor of ORM—and I have no desire to go back.

Naturally, the iPhone a certain NDA-bound platform of indeterminate nature has no ORM layer for the bundled SQLite library.

So I was pretty thrilled to stumble onto Jeff LaMarche’s SQLite Persistent Objects. Think Active Record, except instead of the database telling your model what it’s supposed to look like, the model tells the database what it should look like. Jeff calls the pattern “reverse Active Record”, but I think it looks a lot like Data Mapper.

The best part is that it works exactly like you’d expect:

Person *person1 = [[Person alloc] init];
person1.givenName  = @"John";
person1.familyName = @"Doe";
person1.birthdate  = [NSDate dateWithString: @"1908-11-24 04:12:00 -0800"];
[person1 save]; // Inserts a new row
[person1 release];

Person *person2 = [[Person alloc] init];
person2.givenName  = @"John";
person2.familyName = @"Smith";
person2.birthdate  = [NSDate dateWithString: @"1945-09-13 03:54:00 -0800"];
[person2 save]; // Inserts a new row
[person2 release];

// Oh wait, they both go by Jack

NSArray *people = [Person findByGivenName: @"John"];
for (Person *person in people) {
    person.givenName = @"Jack";
    [person save];
}

It’s still in the very, very early phases of development (it’s at revision 5), so there are some issues to work out, like what happens when the schema changes.

There are also currently a few bugs preventing SQLite Persistent Objects from, uh, compiling at all. I’ve submitted a patch (removed) that addresses these issues. I’ll update this entry when it’s been applied to trunk. (Update: It appears to have been patched.)

Regardless, it’s worth keeping an eye on, even if you don’t use it right away. Seriously, download the source and check it out.

Major bug with BlackBerry browser and multiple cookies

Posted on August 27th, 2008 in Bugs, Mobile | 9 Comments »

This is a long post.

Having investigated this issue for the last few days, I believe that there is a significant issue with the cookie implementation in BlackBerry browsers using the default Internet Browser. I haven’t been able to test the recently-released BlackBerry Bold (9000), which is bundled with the new BlackBerry Browser, but as far as I can determine, devices like the 8800 and 8820 are affected.

The problem occurs when a site attempts to set multiple cookies. Although they are stored on the device, the cookies are returned to the server haphazardly. Sometimes all cookies are returned; more often it’s only the first or last cookie that was set that is passed back.

The following is a TCP dump of traffic to a particular server. I’ve bolded the relevant parts and changed a few names. The server is not load-balanced, and the headers are not altered on my end in any way.

GET / HTTP/1.0
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
x-wap-profile: "http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf"
Accept: application/vnd.rim.html,text/html,application/xhtml+xml,application/vnd.wap.xhtml+xml,text/vnd.sun.j2me.app-descriptor,image/vnd.rim.png,image/jpeg,application/x-vnd.rim.pme.b,image/gif;anim=1,application/vnd.rim.jscriptc;v=0-8-8,application/x-javascript,application/vnd.rim.css;v=1,text/css;media=handheld,application/vnd.wap.wmlc;q=0.9,application/vnd.wap.wmlscriptc;q=0.7,text/vnd.wap.wml;q=0.7,*/*;q=0.5
Accept-Charset: ISO-8859-1,UTF-8,US-ASCII,UTF-16BE,windows-1252,UTF-16LE,windows-1254,KOI8-R,windows-1251,windows-1255,windows-1256,windows-1250
Accept-Language: en-US,en;q=0.5
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
Host: cookie.example.com
Via: BISB_3.3.0.45, 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:56:19 GMT
Set-Cookie: cookie1=o4s9o298mbqjf5qnjmgad76rs0; path=/; domain=cookie.example.com
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:56:19 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=2d14a216211bc7f1a1b03fa747d1087e; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Content-Length: 7975
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8

Here we set three cookies: cookie1, cookie2, and cookie3.

GET /subsequent/request HTTP/1.0
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
Referer: http://cookie.example.com/
Host: cookie.example.com
Cookie: cookie1=o4s9o298mbqjf5qnjmgad76rs0
Via: 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:56:21 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:56:21 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=a082e222cde7b4aedb6a8b42f2723849; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Content-Length: 3701
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/css

The BlackBerry (an 8820) returns only one cookie back (cookie1), so the server tries to resend the cookies it did not receive. Keep in mind the expiration times here were set in the future for this request.

GET / HTTP/1.0
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
Referer: http://cookie.example.com/
Host: cookie.example.com
Cookie: cookie1=o4s9o298mbqjf5qnjmgad76rs0
Via: 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:56:21 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:56:22 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=270773eec992a03fd63a7b74b2f5ef9a; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Connection: close
Content-Type: text/html; charset=utf-8

This is the same as above. The next GET is really interesting:

GET / HTTP/1.0
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
Referer: http://cookie.example.com/
Host: cookie.example.com
Cookie: cookie2=en_us
Cookie: expires=Thu, 27-Aug-2009 15:57:03 GMT
Via: 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: no-cache, max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:57:05 GMT
Set-Cookie: cookie1=32mrdio18lvpg6cduhnt1prmj5; path=/; domain=cookie.example.com
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:57:05 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=2d0b160a97e20469aad34e01890adbfb; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Connection: close
Content-Type: text/html; charset=utf-8

The BlackBerry sends back cookie2 and “expires” in two headers, as if they are two separate cookies!

For comparison, here’s a similar request from the same device accessing the same URL, but using Opera Mini instead. It performs as expected.

GET / HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Connection: Keep-Alive
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.82, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:14:47 GMT
Set-Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; path=/; domain=cookie.example.com
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 17:14:47 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=1ce651a62bfe62a25faff44e5e5d500b; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 2176
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8

GET /image/example.gif HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Referer: http://cookie.example.com/
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.82, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:14:49 GMT
Last-Modified: Wed, 20 Aug 2008 19:36:32 GMT
ETag: "2c"
Accept-Ranges: bytes
Content-Length: 44
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: image/gif

GET / HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Cache-Control: no-cache
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.186, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:16:08 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Language: en_us
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 2283
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8

GET /subsequent/request HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Referer: http://cookie.example.com/
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.186, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:16:09 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Language: en_us
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 958
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/css

GET /image/example.gif HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Referer: http://cookie.example.com/
If-Modified-Since: Wed, 20 Aug 2008 19:36:32 GMT
If-None-Match: "2c"
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.186, unknown

HTTP/1.1 304 Not Modified
Date: Wed, 27 Aug 2008 17:16:10 GMT
Connection: Keep-Alive
Keep-Alive: timeout=5, max=98
ETag: "2c"

So what’s the solution in this situation? It looks like your best option is to set just one cookie—but shove all your values into it and delimit them in some way (and make sure it’s less than 1024 bytes). If you have multiple expire times, though, you will also have to handle expiration manually. Google does this:

Set-Cookie: PREF=ID=xxxxxxxxxxxxxxxx:TM=1219863220:LM=1219863220:S=yyyyyyyyyyyyyyyy; expires=Fri, 27-Aug-2010 18:53:40 GMT; path=/; domain=.google.com

Can everyone just buy an iPhone already?