random PHP functions

I’m still doing a fair amount of PHP work. Right now, it’s all Drupal, for a site we’re rolling out very soon. I keep stumbling across random PHP functions I hadn’t heard of before, and that turn out to be nice little time savers. Two examples:

  1. curl_setopt_array: I used to just call curl_setopt() a bunch of times to set all my options Now I can set a bunch of options in one fell swoop.
  2. http_build_query: Nothing I couldn’t previously do with simple string concatenation, but this is much cleaner.

WIndows 8, Mountain Lion, and Ubuntu 12

I have to do a 10pm web site rollout tonight, so I find myself at home with some time to kill. I haven’t gotten much of a chance to play around with Windows 8, so I decided to download the 90-day eval, and install it on my old laptop. I have the ISO downloaded and ready to go now. However, I had installed Ubuntu 11 on the laptop back in February. I haven’t really played around with it much since then, and I was ready to wipe it out, but when I turned it on, I got an update message letting me know that I could update it to Ubuntu 12.04 LTS. Well, I decided I’d rather upgrade the Ubuntu install on this laptop rather than wiping it out and starting over with Windows 8. It’s running now, and seems to be chugging along smoothly.

I did a little searching, and it looks like 12.04.1 was only just released. There’s an article about it on ZDNet, dated yesterday. And I guess the original 12.04 release was a few months back, based on the date on this Lifehacker article.

There’s been a lot of OS-related news lately, with Mountain Lion just released and Windows 8 nearing general availability. My old 2007 MacBook can’t handle Mountain Lion, so I’m sticking with plain-old Lion on that for now. I’m tentatively planning to buy myself a new MacBook Pro early next year, but I’m not really that worried about it right now. And I’m curious about Windows 8, but not that enthusiastic about it, given what I already know. I read an interesting CNET article this morning, comparing Mountain Lion and Windows 8. I think I agree with his conclusions, for the most part.

I will likely upgrade both my Windows desktop and laptop to Windows 8, when the consumer version is released, but I’m not that excited about it. Meanwhile, maybe I’ll play around with Ubuntu a bit more!

Mandrill REST API

OK, here’s another REST API example. In the same system I blogged about yesterday, I also had to use the Mandrill API to send out some transactional emails. Mandrill is a relatively new service from MailChimp that can be used to send out e-mail via simple API calls. It’s pretty nice, and free to use, up to a certain point.

You can actually use it via regular old SMTP, with some control info added into the headers, or you can go ahead and use their REST API. I decided to use the API. Initially, I had no luck with that at all. After thrashing around a bit, I eventually figured out that I had to update to the newest version of EasyHttp. Then, it started working fine, no problem. I didn’t bother looking into the internals of EasyHttp to try and figure out what was causing the problem. I’m kind of curious, but, as usual, it’s more important to just get the thing working.

So, here’s another Gist with some code:

/*
 * Code/Mandrill.cs
 * These are calls to the Mandrill email service.
 * Reference: https://mandrillapp.com/api/docs/
 * 
 * ajh 2012-08-09: new
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Configuration;
using System.Dynamic;
using EasyHttp.Http;
using System.Net;
using log4net;

namespace Sample.Web
{
    enum MandrillError
    {
        OK,
        WebException,
        HttpNotOk,
        Invalid,
        Rejected,
        Unknown
    }

    public class Mandrill
    {
        static string MandrillBaseUrl = ConfigurationManager.AppSettings["MandrillBaseUrl"];
        static Guid MandrillKey = new Guid(ConfigurationManager.AppSettings["MandrillKey"]);

        public static bool SendActivationEMail(BLL.TrialSignup ts, out string errorMsg)
        {
            string activationLink =
                HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + "/Register/Activation.aspx?id=" + ts.Id;
            
            //send-template(string key, string template_name, array template_content, struct message) 
            dynamic sendParams = new ExpandoObject();
            sendParams.key = MandrillKey;
            sendParams.template_name = "Secret Project Trial Activation";

            sendParams.template_content = new List();
            
            sendParams.message = new ExpandoObject();
            sendParams.message.subject = "Here's your Secret Project activation email";
            sendParams.message.from_email = "info@SecretProject.com";
            sendParams.message.from_name = "Secret Project";

            sendParams.message.to = new List();
            sendParams.message.to.Add(new ExpandoObject());
            sendParams.message.to[0].email = ts.EMail;
            sendParams.message.to[0].name = ts.Name;

            sendParams.message.track_opens = true;
            //sendParams.message.track_clicks = true;

            sendParams.message.global_merge_vars = new List();
            sendParams.message.global_merge_vars.Add(new ExpandoObject());
            sendParams.message.global_merge_vars[0].name = "NAME";
            sendParams.message.global_merge_vars[0].content = ts.Name;

            sendParams.message.global_merge_vars.Add(new ExpandoObject());
            sendParams.message.global_merge_vars[1].name = "LINK";
            sendParams.message.global_merge_vars[1].content = activationLink;

            errorMsg = string.Empty;
            
            MandrillError merr = SendMessage(sendParams);

            switch (merr)
            {
                case MandrillError.OK:
                    return true;
                case MandrillError.WebException:
                case MandrillError.HttpNotOk:
                    errorMsg = "There was an issue sending your activation e-mail. Please try again later or call us directly.";
                    break;
                case MandrillError.Invalid:
                    errorMsg = "Your email address appears to be invalid. Please try again with a valid address, or call us directly.";
                    break;
                case MandrillError.Rejected:
                    errorMsg = "Your activation email was rejected. Please try again with a valid address, or call us directly.";
                    break;
                case MandrillError.Unknown:
                    errorMsg = "There was an unknown problem sending your activation email. Please try again, or call us directly.";
                    break;
            }
            return false;
        }

        public static bool SendSalesNotification(BLL.TrialSignup ts)
        {
            dynamic sendParams = new ExpandoObject();
            sendParams.key = MandrillKey;
            sendParams.template_name = "Secret Project Trial Sales Notification";

            sendParams.template_content = new List();

            sendParams.message = new ExpandoObject();
            sendParams.message.subject = "Secret Project Trial Account Notification";
            sendParams.message.from_email = "info@SecretProject.com";
            sendParams.message.from_name = "Secret Project";

            sendParams.message.to = new List();
            sendParams.message.to.Add(new ExpandoObject());
            sendParams.message.to[0].email = ConfigurationManager.AppSettings["SalesEmail"];
            sendParams.message.to[0].name = "Secret Project Sales";

            //sendParams.message.track_opens = true;
            //sendParams.message.track_clicks = true;

            sendParams.message.global_merge_vars = new List();
            sendParams.message.global_merge_vars.Add(new ExpandoObject());
            sendParams.message.global_merge_vars[0].name = "NAME";
            sendParams.message.global_merge_vars[0].content = ts.Name;

            sendParams.message.global_merge_vars.Add(new ExpandoObject());
            sendParams.message.global_merge_vars[1].name = "COMPANY";
            sendParams.message.global_merge_vars[1].content = ts.CompanyName;
            
            sendParams.message.global_merge_vars.Add(new ExpandoObject());
            sendParams.message.global_merge_vars[2].name = "EMAIL";
            sendParams.message.global_merge_vars[2].content = ts.EMail;

            MandrillError merr = SendMessage(sendParams);

            switch (merr)
            {
                case MandrillError.OK:
                    return true;
                case MandrillError.WebException:
                case MandrillError.HttpNotOk:
                case MandrillError.Invalid:
                case MandrillError.Rejected:
                case MandrillError.Unknown:
                    break;
            }
            return false;
        }

        private static MandrillError SendMessage(dynamic sendParams)
        {
            ILog _log = log4net.LogManager.GetLogger("Mandrill/SendMessage");

            string url = MandrillBaseUrl + "/messages/send-template.json";

            var http = new HttpClient
            {
                Request = { Accept = HttpContentTypes.ApplicationJson }
            };

            EasyHttp.Http.HttpResponse response;
            try
            {
                response = http.Post(url, sendParams, HttpContentTypes.ApplicationJson);
            }
            catch (WebException ex)
            {
                _log.ErrorFormat("Error: WebException - {0}", ex.Message);
                return MandrillError.WebException;
            }

            if (response.StatusCode != HttpStatusCode.OK)
            {
                _log.InfoFormat("Response = {0} - {1}", response.StatusCode, response.StatusDescription);
                _log.Info(response.RawText);
                return MandrillError.HttpNotOk;
            }

            dynamic rv = response.DynamicBody;
            _log.InfoFormat("email: {0}, status: {1}", rv[0].email, rv[0].status);

            string send_status = rv[0].status;
            if (send_status == "sent" || send_status == "queued")
                return MandrillError.OK;

            // otherwise, it should be "rejected" or "invalid"
            if (send_status == "invalid")
            {
                return MandrillError.Invalid;
            }
            if (send_status == "rejected")
            {
                return MandrillError.Rejected;
            }

            // unexpected...
            return MandrillError.Unknown;
        }
        
    }
}

I hope someone finds it useful and/or entertaining.
In this code, I’m sending out two e-mails, one is a trial account activation email, and the other is just a notification to sales. On the first, I’m looking at the errors closely and trying to return a useful error message, so the customer knows that something has gone wrong and has a clue about how to handle it. On the second, I’m less worried about that, as it’s just a notification to our salesperson.

Zoho CRM REST API

I have about a dozen programming-related items floating around in my head that I’d like to write up short blog entries about. Maybe I’ll manage to get one or two written up tonight.

I’ve been doing some work recently with some random REST APIs. When I’m calling REST APIs from .NET, I like to use EasyHttp. Based on the stats on its NuGet page, I guess it’s not that popular, but it’s updated often enough, and it works well for me.

I recently worked on a project where I had to create a lead in Zoho CRM, using their API. I had a little bit of trouble, but only because I didn’t have quite the right parameters set — it turns out that “newformat=1” is really important. I had looked for some sample C# code for the Zoho API when I started this, and didn’t find any, so I thought I’d post a quick Gist with my code:

/*
 * Sample C# code to create a lead with the Zoho CRM API.
 * ajh 2012-08-23
 */
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Dynamic;
using System.Linq;
using System.Net;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using EasyHttp.Http;
using log4net;

namespace Sample.Web
{
    public class ZohoCrm
    {
        static string ZohoApiBaseUrl = "https://crm.zoho.com/crm/private/xml/Leads/insertRecords"; 
        static string ZohoApiKey = ConfigurationManager.AppSettings["ZohoApiKey"];

        public static bool CreateLead(BLL.SampleLead sl)
        {
            ILog _log = log4net.LogManager.GetLogger("ZohoCrm/CreateLead");

            XDocument xmlData = new XDocument(
            new XElement("Leads",
                new XElement("row", new XAttribute("no", "1"),
                    new XElement("FL", new XAttribute("val", "Lead Source"), "Trial Signup"),
                    new XElement("FL", new XAttribute("val", "Company"), sl.CompanyName),
                    new XElement("FL", new XAttribute("val", "Last Name"), sl.Name),
                    new XElement("FL", new XAttribute("val", "Email"), sl.EMail)
                )));

            string url = string.Format("{0}?authtoken={1}&scope=crmapi&newFormat=1&xmlData={2}", 
				ZohoApiBaseUrl, ZohoApiKey, 
                HttpUtility.UrlEncode(xmlData.ToString()));

            var http = new HttpClient
            {
                Request = { Accept = HttpContentTypes.ApplicationXml }
            };

            dynamic emptyPost = new ExpandoObject();            
            EasyHttp.Http.HttpResponse response;
            try
            {
                response = http.Post(url, emptyPost, HttpContentTypes.ApplicationXml);
            }
            catch (WebException ex)
            {
                _log.ErrorFormat("Error: WebException - {0}", ex.Message);
                return false;
            }

            if (response.StatusCode != HttpStatusCode.OK)
            {
                _log.InfoFormat("Response = {0} - {1}", response.StatusCode, response.StatusDescription);
                _log.Info(response.RawText);
                return false;
            }

            XDocument xdoc;
            try
            {
                xdoc = XDocument.Parse(response.RawText);
            }
            catch (XmlException ex)
            {
                _log.ErrorFormat("Error: XmlException parsing API response - {0}", ex.Message);
                _log.Info(response.RawText);
                return false;
            }

            string msg;
            try
            {
                msg = xdoc.Descendants("result").First().Element("message").Value;
            }
            catch (Exception ex)
            {
                _log.ErrorFormat("Error: Exception reading from API response - {0}", ex.Message);
                _log.Info(response.RawText);
                return false;
            }
            if (msg != "Record(s) added successfully")
            {
                _log.InfoFormat("Unexpected XML result: {0}", msg);
                _log.Info(response.RawText);
                return false;
            }

            return true;
        }
    }

(See also this Gist.)

I’m also using log4net here.  I’m a really big fan of log4net, and I use it in almost all of my projects. I have a pretty standard log.config file I use that sets up a RollingFileAppender, with ten files of 100k each. That’s usually enough to keep a few days or weeks worth of history, depending on the level of logging and the level of activity. And I never have to worry about the log files growing out of control; they just keep rolling over. I think log4net is pretty popular, based on the numbers on their NuGet page.

I try to do a lot of error-handling whenever I’m dealing with a REST API. There are plenty of things that can go wrong. You can see in the code above that I try to trap anything that might throw an exception and log it. I’m never sure if I’m doing this the “right” way. I know some people, when writing a routine like this, would just let the exceptions happen, then have a try/catch around the call to the CreateLead method, and deal with it at a higher level.

Ghosts

I’ve only just now figured out that Ted Leo’s “Ghosts” is a cover of The Jam’s “Ghosts”. I’ve probably listened to Ted’s version 100 times in the last few years. I hadn’t really listened to The Jam at all recently, until digging out my copy of At the BBC last week. Listened to the first CD last Saturday, and I’m listening to the second CD today. Good stuff!

Classic ASP – SQL Injection

We still have a few old sites at work that are in classic ASP. One of the problems that tends to occur on these old sites is SQL injection attacks. A lot of old ASP code was written without taking SQL injection into account. It would be great if we could just rewrite all of these sites in ASP.NET, but sometimes the client isn’t interested in doing that.

Well, yesterday, we had a production ASP site get hit. This is a site for an old client that I’ve personally never worked on, so I really knew nothing about it, but I’m getting dragged in, now that we need to clean it up. Looking at the site, I’m actually surprised this hadn’t happened earlier.

Looking at the code, I see a few places where we could be doing a better job of input validation. And also a few places where we’re doing un-parameterized SQL, which is a big no-no if you want to avoid SQL injection. So, I’m going to try to clean some of that up.

I also want to use URLScan on the server, with some SQL injection rules, to try to get some of this stuff caught at the IIS level. I found this article on how to add some rules to the URLScan.ini file to mitigate SQL injection attacks. (I actually first started reading this article, but then remembered that this particular web server is still on IIS 6.)

When I started poking around on the server, I was surprised to see tha URLScan was already installed. However, it was not configured to do any SQL injection prevention. So, Monday morning, I’m going to try to add the SQL injection rules to the ini file, and see if that breaks anything. Then, I hope to have time to tighten up the code a bit and roll out a new version. I can’t say I’m excited to be working on nearly decade-old VBScript, but hey, it’ll be good for a few laughs, right?

Using SSH with Mercurial

We’ve been using Mercurial and Bitbucket at work for quite a while now. Things have been going pretty smoothly, and I don’t regret the choice of Hg over Git or TFS at all. Nor do I regret using Bitbucket as a back-end. They’ve had occasional outages, but probably no more than we’d have with Github or any other web-based service.

I had a bit of a problem pushing a really big changeset today though, so I decided to switch from HTTPS to SSH. It took about 20 minutes to set up, following these instructions. And I had to look here for some additional information on Pageant. I wish there had been a smoother, faster, way to get the whole thing set up, but it’s running smoothly now that I’ve got it all figured out.

NYC Drupal Camp

I went to the NYC Drupal Camp at Columbia this weekend. I only made it out on Saturday, but I would have liked to have gone today also, if I didn’t have other things to take care of.

The sessions I went to were all great (which isn’t always the case at free “code camp” events like this). The first session I attended was on node access, by Ken Rickard. It was a well-presented talk on a fairly dry subject. I’m not sure I’ll have cause to use much of the information in the talk any time soon, but it’s good to know.

The next was on using SQL within Drupal, by David Diers. His slides are here. This was a pretty basic talk, and I already knew the basics of both db_query and db_select, but I didn’t know some of the specifics, so the talk was useful and applicable to the kind of stuff I’m working on.

The next talk was on the Migrate module, given by Ashok Modi. He has his slides up here and a blog post covering similar ground here. This one would have been a great help to me a few months back, when I was trying to figure out how to import a lot of data into a Drupal site we’re working on at EVI. After this talk, I realize I did it the hard way!

After lunch, I went to a session on caching. Their presentation is available on Github. I’m not too familiar with Drupal’s caching, or Apache’s, or with third-party accelerators, so this was all good stuff for me. I’m curious about Varnish now, and I may follow up on that.

After that, I attended a session on Drupal Commerce, given by Richard Jones. I’m probably going to use Drupal Commerce on an upcoming project, so it was good to get a little more info on it.

Finally, I went to a session on hacking Drupal by Ben Jeavons. Very informative. It looks like XSS attacks are the most common problem for Drupal sites. He talked about using the Vuln module to identify problems, which sounded pretty good, but it looks like that module hasn’t been updated for Drupal 7. His slide on handling strings safely was useful; I might need to print it out and keep it handy. I need to remember those functions — check_plain(), check_url(), check_markup(), and filter_xss().

So, overall, a good day. I wish I could have gone back for more today. The main purpose in writing this blog post, by the way, was to get some of this stuff out of my head, in the hope that writing it up will help me retain some of the information. When I go to one of these things, and sit through a half-dozen 45 minute presentations in one day, it’s easy for the information to fade quickly. I’m hoping that writing this stuff up will help me remember.

Wilco – Shot In The Arm

Listening to Summerteeth today. I’d forgotten how good it was.

Maybe all I need is a shot in the arm.
Something in my veins, bloodier than blood.

What you once were isn’t what you want to be anymore.

And searching for “shot in the arm” on Google found an old article about the band’s breakup with Reprise Records over Yankee Hotel Foxtrot. How wrong was Reprise about that?