#!/usr/bin/perl
#
# Copyright 2009 Graphcomp - ALL RIGHTS RESERVED
#
# Author: Bob Free - bfree@graphcomp.com
our $DEBUG = 0;
our $SiteID = 2;
my $params = $ENV{'QUERY_STRING'};
use CGI;
use MIME::Base64;
use Digest::MD5;
our @EncodeEscapes = split('',q{ !"*'();:@&=+$,/?%#[]});
our $EncodeEscapeCount = scalar(@EncodeEscapes);
# Set up mail gateway and DB
my($gateway,$dbname);
if ($ENV{HTTP_HOST} ne 'graphcomp.org')
{
use lib "/services/webpages/util/g/c/gc_admin.site.aplus.net/";
$gateway = '/usr/sbin/sendmail';
}
else
{
$gateway = 'smtp.sbcglobal.net';
}
my $cgi = new CGI();
my $udid = $cgi->param('udid') || $cgi->url_param('udid');
my $seed = $cgi->param('seed') || $cgi->url_param('seed');
my $appv = $cgi->param('appv') || $cgi->url_param('appv');
my($count,$challenge,$delta,$client,$model,$version,$locale,$deviceName,$rest,$check);
my $from_iphone = isIPhone($udid,$seed,$appv);
# if not from iPhone display instructions
if (!$from_iphone)
{
displayForm();
exit 0;
}
my $appVer = $appv;
$appVer =~ s|_| |g;
my($app,$ver) = split('-',$appVer);
# Connect to DB
use gcLib::DBX;
my $db = new gcLib::DBX('graphcomp',1);
if(!$db)
{
displayError("Unable to access user records at this time - please try again later");
exit 0;
}
my $dbh = $db->{dbh};
# Check for product
my($prodID) =
$db->QueryRow("select ID,SKU from Products where Name='$app'");
if (!$prodID)
{
# Add product, if not already there
$db->Exec("insert Products set Name='$app'");
($prodID) =
$db->QueryRow("select ID from Products where Name='$app'");
if (!$prodID)
{
displayError("Unable to access product records at this time - please try again later");
exit 0;
}
}
# Check for installation
my($instID,$contactID,$devName,$devStatus) =
$db->QueryRow("select ID,ContactID,DeviceName,Status from Installation where DeviceID='$udid'");
our $is_tester = (defined($devStatus) && ($devStatus & 2));
if (!$instID)
{
# Add installation, if not already there
$db->Exec(qq
{
insert Installation set
DeviceID='$udid',
Model='$model',
OS='MacOS iPhone SDK',
OSVersion='$version',
Locale='$locale',
DeviceName='$deviceName',
Status=1;
});
($instID) =
$db->QueryRow("select ID from Installation where DeviceID='$udid'");
if (!$instID)
{
displayError("Unable to access installation records at this time - please try again later");
exit 0;
}
}
else
{
# Update installation
$db->Exec(qq
{
update Installation set
OSVersion='$version',
Locale='$locale',
DeviceName='$deviceName'
where ID=$instID;
});
}
# Check for product installation
my($prodInstID) =
$db->QueryRow(qq
{
select ID from ProductInstallation
where ProductID=$prodID and InstallationID=$instID;
});
if (!$prodInstID)
{
# Add product installation, if not already there
$db->Exec(qq
{
insert ProductInstallation set
productID=$prodID,
InstallationID=$instID,
ProductVersion='$ver',
FirstContact=now(),
LastContact=now(),
WanIP='$ENV{REMOTE_ADDR}',
LanIP='$client';
});
}
else
{
# Update product installation
$db->Exec(qq
{
update ProductInstallation set
ProductVersion='$ver',
LastContact=now(),
WanIP='$ENV{REMOTE_ADDR}',
LanIP='$client'
where ID=$prodInstID;
});
}
# Get Subscription IDs
our($newsSubID) = $db->QueryRow("select ID from Subscriptions where Name='iPhone News'");
our($betaSubID) = $db->QueryRow("select ID from Subscriptions where Name='iPhone Beta Application'");
our($testSubID) = $db->QueryRow("select ID from Subscriptions where Name='iPhone Beta testers'");
# Has contact ID
my($oldEmail,$verified,$newsID,$optin_news,$betaID,$optin_beta,$updates);
$optin_news = 1;
$optin_beta = 0;
if ($contactID)
{
# Check email
($oldEmail,$verified) = $db->QueryRow("select Email,Validated from Contacts where ID=$contactID");
}
my $news = $cgi->param('news') || $cgi->url_param('news');
my $beta = $cgi->param('beta') || $cgi->url_param('beta');
my $email = lc($cgi->param('email') || $cgi->url_param('email'));
my $submit = $cgi->param('submit') || $cgi->url_param('submit');
# Populate form
if (!$submit)
{
$email = $oldEmail;
$news = $optin_news ? 'YES' : '';
$beta = $optin_beta ? 'YES' : '';
displayForm();
exit 0;
}
#print "Content-type: text/html\n\n";
# Check for email
if (!$email)
{
displayError('You must supply an email address');
exit 0;
}
# Test email
my($ename,$domain);
if ($email =~ m|([^\@+]\@(.+\..+))|)
{
$ename = $1;
$domain = $2;
}
if (!$ename || !$domain)
{
displayError('You must supply a valid email address');
exit 0;
}
# Set up email
use gcLib::Send;
my $gw = new gcLib::Send($gateway);
# Check for existing email
my($existID) = $db->QueryRow("select ID from Contacts where Email='$email'");
if ($existID)
{
if (!$contactID)
{
linkAccounts($existID,$instID,$email);
exit 0;
}
elsif ($contactID != $existID)
{
switchAccounts($existID,$instID,$oldEmail,$email);
exit 0;
}
}
# Create new member
if (!$contactID)
{
use gcLib::Auth;
my $auth = new gcLib::Auth();
newMember($auth,$instID,$ename,$email,$ENV{REMOTE_ADDR});
exit 0;
}
# Existing member
$updates = updateSubscriptions($contactID);
if ($oldEmail ne $email)
{
if (!$oldEmail || !$verified)
{
updateEmail($contactID,$email);
}
else
{
changeEmail($contactID,$oldEmail,$email);
}
}
elsif (!$verified)
{
verifyEmail($contactID,$email);
displayStatus("An email has been sent to $email for verification");
}
elsif ($updates)
{
displayStatus('Your subscription status has been updated');
}
else
{
displayStatus('No change to subscriptions');
}
exit 0;
sub isIPhone
{
my($udid,$seed,$appv) = @_;
debugDie('No UDID') if (!$udid);
debugDie('No seed') if (!$seed);
debugDie('No appv') if (!$appv);
debugDie('Bad UDID format') if (!validateUDID($udid));
my $appVer = $appv;
$appVer =~ s|_| |g;
my($app,$ver) = split('-',$appVer);
my $decode = Swizzle($appv.'graphcomp'.$udid,$seed);
#print "$decode
\n";
#my $data = UrlDecode($decode);
my $data = decode_base64($decode);
#print "$data\n";
($count,$challenge,$delta,$client,$model,$version,$locale,$deviceName,$rest) =
split('\|',$data,9);
# print "Content-type: text/html\n\n".
# "$count,$challenge,$delta,$client,$model,$version,$locale,$deviceName,$rest\n";
my $len = length($rest);
#print "$count, $len\n";
debugDie("Invalid seed markers: $count,$len") if ($count != $len);
# Test challenge
my $life = 10;
my $time = time();
my $stamp = int(($time-$delta)/$life);
my $md5 = new Digest::MD5();
$md5->add('graphcomp');
$md5->add($ENV{HTTP_HOST});
$md5->add($ENV{REMOTE_ADDR});
$md5->add($stamp);
$md5->add($client);
$check = uc($md5->hexdigest());
#print "challenge: $challenge, $check\n";
return debugDie("Invalid challenge match: $challenge,$check") if ($challenge ne $check);
#if ($challenge ne $check)
#{
# print qq
# {Content-type: text/html\n\n
#
#challenge: $challenge, $check
#$delta
#$ENV{HTTP_HOST}
#$ENV{REMOTE_ADDR}
#$stamp
#$client
#
# };
# return 0;
#}
return $model =~ m/^(iPhone|iPod)/;
}
sub validateUDID
{
my($code) = @_;
return $code =~ m|^[a-f0-9]{40}$|;
}
sub updateSubscriptions
{
my($contactID) = @_;
my $updates = 0;
# Check News subscription
($newsID,$optin_news) = $db->QueryRow(qq
{
select ID,OptIn
from Subscribers
where ContactID=$contactID and
SubscriptionID=$newsSubID;
});
$optin_news = 1 if (!defined($optin_news));
my $new_news = ($news eq 'YES') ? 1 : 0;
if (!$newsID)
{
# Add subscription, if not already there
$db->Exec(qq
{
insert Subscribers set
contactID=$contactID,
SubscriptionID=$newsSubID,
OptIn=$new_news;
});
$updates++;
}
else
{
# Update subscription
$db->Exec(qq
{
update Subscribers set
OptIn=$new_news
where ID=$newsID;
});
$updates++ if ($optin_news != $new_news);
}
$optin_news = $new_news;
# Check Beta subscription
($betaID,$optin_beta) = $db->QueryRow(qq
{
select ID,OptIn
from Subscribers
where ContactID=$contactID and
SubscriptionID=$betaSubID;
});
$optin_beta = 0 if (!defined($optin_beta));
my $new_beta = ($beta eq 'YES') ? 1 : 0;
if (!$betaID)
{
# Add subscription, if not already there
$db->Exec(qq
{
insert Subscribers set
contactID=$contactID,
SubscriptionID=$betaSubID,
OptIn=$new_beta;
});
$updates++;
}
else
{
# Update subscription
$db->Exec(qq
{
update Subscribers set
OptIn=$new_beta
where ID=$betaID;
});
$updates++ if ($optin_beta != $new_beta);
}
$optin_beta = $new_beta;
return $updates;
}
sub updateEmail
{
my($contactID,$email,$oldEmail) = @_;
my $save = "LastValidEmail='$oldEmail'," if ($oldEmail);
# Update email
$db->Exec(qq
{
update Contacts set $save
Email='$email',
Validated=0
where ID=$contactID;
});
verifyEmail($contactID,$email);
}
sub verifyEmail
{
my($contactID,$email) = @_;
my $params = Package('graphcompiphoneactivate',"$contactID,$email");
my $url = "http://clydomania.com/activate.cgi?req=$params";
my $text = MakeUpdateText($url);
my $html = MakeUpdateHtml($url);
$gw->Send('iphone@graphcomp.com',$email,'Clydomania Activation',$text,$html);
displayStatus(qq
{
An email has been sent to $email for verification; follow the email instructions
in order to activate your Clydomania account.
});
}
sub oldEmail
{
my($contactID,$oldEmail,$email) = @_;
my $params = Package('graphcompiphonebadrequest',"$contactID,$oldEmail,$email");
my $url = "http://clydomania.com/badrequest.cgi?req=$params";
my $text = qq
{
A request has been made from an $model ($deviceName) to update
your Clydomania account from $oldEmail to $email.
You will receive a second email at $email to verify your new email address.
If this request did _not_ originate from you, please browse to the following URL:
$url
Thank you for notifying us in this matter.
};
my $html = qq
{
A request has been made from an $model ($deviceName) to update
your Clydomania account from $oldEmail to $email.
You will receive a second email at $email to verify your new email address.
If this request did not originate from you, please
Notify Us.
Thank you for your asistance in this matter.
};
$gw->Send('iphone@graphcomp.com',$oldEmail,'Clydomania Email Change',$text,$html);
}
sub displayForm
{
header();
form();
footer();
}
sub displayStatus
{
my($status) = @_;
my $msg = qq{$status};
header($msg);
form();
footer();
}
sub displayError
{
my($status) = @_;
my $msg = qq{$status};
header($msg);
form();
footer();
}
sub displayThanks
{
my $path = $ENV{SCRIPT_NAME};
$path =~ s|^/(.*)/[^/]+&|$1/thanks.cgi|;
print "Location: http://$ENV{HTTP_HOST}/$path?$params\n\n";
}
# Change email for a contact linked to this device
sub changeEmail
{
my($contactID,$oldEmail,$email) = @_;
oldEmail($contactID,$oldEmail,$email);
updateEmail($contactID,$email,$oldEmail);
}
# Create a new member linked to this device
sub newMember
{
my($auth,$instID,$ename,$email,$ip) = @_;
my $uname = '';
my $temp = $ename;
my $i = 1;
while (1)
{
my($uhash) = $db->QueryRow("select UserHash from Contacts where UserName='$temp'");
last if (!$uhash);
$temp = $uname.++$i;
}
$uname = $temp;
$db->Exec(qq
{
insert Contacts set
UserName='$uname',
Email='$email',
LastIP='$ip',
BlockMail=1,
Validated=0,
ProfilePublic=0,
StartDate=now()
});
my($ID,$StartDate) = $db->QueryRow("select ID,StartDate from Contacts where UserName='$uname'");
if (!$ID)
{
displayError('Unable to add new user - try again later');
exit 0;
}
my $sID = 0;
my($gID) = $db->QueryRow("select ID from Groups where Name='member'");
if ($gID)
{
$db->Exec("insert GroupMembers set ContactID=$ID,GroupID=$gID,SiteID=$SiteID");
}
my $uhash = $auth->GenUserHash($ID,$StartDate);
my $paswd = $auth->GenPassword();
my $phash = $auth->GenPasswordHash($uhash,$paswd);
$db->Exec(qq
{
update Contacts set
UserHash='$uhash',
PassHash='$phash'
where ID=$ID
});
$db->Exec("update Installation set ContactID=$ID where ID=$instID");
updateSubscriptions($ID);
my $params = Package('graphcompiphoneactivate',"$ID,$email");
my $url = "http://clydomania.com/activate.cgi?req=$params";
my $text = qq
{
Welcome to Clydomania!
Verifying your email allows you to subscribe to our newsletters, receive
info on updates and apply to receive early betas.
If you have the full version of Clydomania, you can submit and download
animations, as well as browse and vote for your favorite Clydomania animations.
To activate your account, please browse to the following URL:
$url
Thank you for Joining Clydomania.
Please visit our site at http://clydomania.com/
Clydomania is brought to you by Grafman Productions, a subsidiary of
Graphcomp. Clydomania, Clydomania Lite and Grafman Productions are
trademarks of Graphcomp.
};
my $html = qq
{
Welcome to Clydomania!
Verifying your email allows you to subscribe to our newsletters, receive
info on updates and apply to receive early betas.
If you have the full version of Clydomania, you can submit and download
animations, as well as browse and vote for your favorite Clydomania animations.
To use these services, please Activate your account.
Thank you for Joining Clydomania.
Please visit our site at clydomania.com
Clydomania is brought to you by Grafman Productions, a subsidiary of
Graphcomp. Clydomania, Clydomania Lite and Grafman Productions are
trademarks of Graphcomp.
};
$gw->Send('iphone@graphcomp.com',$email,'Clydomania Activation',$text,$html);
displayStatus(qq
{
An email has been sent to $email for verification; follow the email instructions
in order to activate your Clydomania account.
});
}
# Link this device to an existing contact
sub linkAccounts
{
my($existID,$instID,$email) = @_;
my $new_news = ($news eq 'YES') ? 1 : 0;
my $new_beta = ($beta eq 'YES') ? 1 : 0;
my $params = Package('graphcompiphoneswitch',"$existID,$instID,$email,undef,
$newsSubID,$new_news,$betaSubID,$new_beta");
my $url = "http://clydomania.com/switch.cgi?req=$params";
my $text = qq
{
A request has been made from an $model ($deviceName) to link this
device to your existing Clydomania account $email.
To approve this change, please browse to:
$url
If this request did not originate from you, please email
the above link to iphone\@graphcomp.com with the subject 'Invalid Change Request',
or simply ignore this message.
};
my $mailto = UrlEncode('mailto:iphone@graphcomp.com?subject="Invalid Change Request"&body='.$url);
my $html = qq
{
A request has been made from an $model ($deviceName) to link
this device to your existing Clydomania account $email.
If you initiated this change, please:
If this request did not originate from you, please
Notify Us,
or simply ignore this message.
};
$gw->Send('iphone@graphcomp.com',$email,'Clydomania Email Change Request',$text,$html);
displayStatus(qq
{
An email has been sent to $email for verification.
Follow the email instructions
to approve this change.
});
}
# Switch contacts linked to this device
sub switchAccounts
{
my($existID,$instID,$oldEmail,$email) = @_;
oldEmail($existID,$oldEmail,$email);
my $new_news = ($news eq 'YES') ? 1 : 0;
my $new_beta = ($beta eq 'YES') ? 1 : 0;
my $params = Package('graphcompiphoneswitch',"$existID,$instID,$email,$oldEmail,
$newsSubID,$new_news,$betaSubID,$new_beta");
my $url = "http://clydomania.com/switch.cgi?req=$params";
my $text = qq
{
A request has been made from an $model ($deviceName) to update
your Clydomania email from $oldEmail to $email.
To approve this change, please browse to:
$url
If this request did not originate from you, please email
the above link to iphone\@graphcomp.com with the subject 'Invalid Change Request',
or simply ignore this message.
};
my $mailto = UrlEncode('mailto:iphone@graphcomp.com?subject="Invalid Change Request"&body='.$url);
my $html = qq
{
A request has been made from an $model ($deviceName) to update
your Clydomania email from $oldEmail to $email.
If you initiated this change, please:
If this request did not originate from you, please
Notify Us,
or simply ignore this message.
};
$gw->Send('iphone@graphcomp.com',$email,'Clydomania Email Change Request',$text,$html);
displayStatus(qq
{
A courtesy notice has been sent to your current email $oldEmail;
a second email has been sent to $email for verification.
Follow the email instructions
to approve this change.
});
}
sub MakeUpdateText
{
my($url) = @_;
my $text = qq
{
A request has been made from an $model ($deviceName) to update
your Clydomania subscription status.
Please activate this account by browsing to:
$url
Once activated, you will received news and notifications based on your
selected opt-in status.
When using the full version of Clydomania, activation also allows you to
share, download and vote for your favorite animations from your $model.
Visit our site at http://clydomania.com/
If this request did not originate from you, please email
the above link to iphone\@graphcomp.com with the subject 'Invalid Activation Request',
or simply ignore this message.
};
return $text;
}
sub MakeUpdateHtml
{
my($url) = @_;
my $mailto = UrlEncode('mailto:iphone@graphcomp.com?subject=Invalid Activation Request&body='.$url);
my $html = qq
{
A request has been made from an $model ($deviceName) to update
your Clydomania subscription status.
Please activate your Clydomania account:
Once activated, you will received news and notifications based on your
selected opt-in status.
When using the full version of Clydomania, activation also allows you to
share, download and vote for your favorite animations from your $model.
Visit our site at clydomania.com
If this request did not originate from you, please
Notify Us,
or simply ignore this message.
};
return $html;
}
sub header
{
my($status) = @_;
$status = ' ' if (!$status);
print qq
{Content-type: text/html; charset=UTF-8
Clydomania - SignUp
};
}
sub form
{
if (!$verified && !$submit)
{
print qq
{
Sign up for Beta releases, or to receive
grafman's newsletter.
};
}
if ($from_iphone)
{
my $news_check = ($optin_news ? 'checked' : '');
my $beta_check = ($optin_beta ? 'checked' : '');
my $beta_string;
if ($is_tester)
{
$beta_string = qq
{
Approved Beta Tester
};
}
else
{
$beta_string = qq
{
Apply to receive early Betas
};
}
my $email_status = ($verified ? 'verified' :
'unverified') if ($email);
print qq
{
};
if (!$verified && !$submit)
{
print qq
{
Your email address will be used for no purpose other than to inform you of updates
and give you access to your online account; it will not be shared, distributed
nor sold.
You may unsubscribe by unchecking the above boxes and pressing Update.
};
}
}
else
{
print qq
{
In order to sign up, please use Clydomania
from your iPhone or iPod Touch
};
}
}
sub Package
{
my($key,$string) = @_;
$string = encode_base64($string);
$string =~ s|[\r\n]+||g;
$string = Swizzle($key,$string);
$string = UrlEncode($string);
return $string;
}
sub UrlEncode
{
my($string) = @_;
my @chars = split('',$string);
my $out = "";
foreach my $char (@chars)
{
my $k = ord($char);
next if (($k < 0) || ($k > 255));
my $e = 0;
if ($k < 128)
{
for ($e=0; $e<$EncodeEscapeCount; $e++)
{
last if ($k == ord($EncodeEscapes[$e]));
}
}
if ($e < $EncodeEscapeCount)
{
$out .= sprintf("%%%02X",$k);
}
else
{
$out .= $char;
}
}
return($out);
}
sub UrlDecode
{
my($string) = @_;
my @chars = split('%(..)',$string);
my $pos = 0;
my $out = "";
foreach my $char (@chars)
{
if (++$pos & 1)
{
$out .= $char;
}
else
{
$out .= chr(hex($char));
}
}
return $out;
}
sub Swizzle
{
my($key,$str) = @_;
my $keyLength = length($key);
my $strLength = length($str);
my @keyChars = split('',$key);
my @strChars = split('',$str);
my $out = "";
my $mid = int($strLength / 2);
for (my $i=0; $i<$strLength; $i++)
{
my $code = ord($strChars[$i]) - 32;
$code = 233 if ($code < 0);
$code ^= ord($keyChars[($mid+$strLength-$i)%$keyLength]) >> 5;
$code += 32;
$out .= chr($code);
}
return $out;
}
sub debugDie
{
my($msg) = @_;
return 0 if (!$DEBUG);
print "Content-type: text/html\n\n$msg";
exit 0;
}