Looking at a project from different angles

For our 15th anniversary my wife and went to the south island of New Zealand, with a long layover in Sydney. We only had a few hours in Sydney so we went to see the Opera House and then walk through the botanical gardens next door.

As we walked around the harbor I took pictures of the opera house from several different angles. And that got me thinking about the advice I’ve been given both about photography and about my work: make sure you try things from different angles.

A classic angle of the Sydney opera house from across the harbor.

Too often all kinds of experts get into a rut and lose track of the perspective non-experts, and other experts with whom they disagree. Cable news channels like to package those ruts as two talking heads yelling at each other by calling it “debate”.

It’s an easy trap to fall into even without watching the people paid to yell at each other. Sometimes when we look at a problem twice it looks different because we changed something small, and we think we’ve seen all the valid angles. But we’ve just reinforced our sense of superiority not actually explored anything interesting yet.

When you look right at the sun a small change can have a large impact, but you may still be fundamentally in the same place with a fundamentally flawed perspective.

And sometimes you look from a new angle and something easily recognizable becomes new and different, but that’s not always an improvement. There are reasons for best practices, and sometimes we just reinvent the wheel when we try to break our own path.

You don't see pictures of the opera house from this angle often – which is probably for the best.
You don’t see pictures of the opera house from this angle often – which is probably for the best.
This angle was even worse. It's a good thing I wasn't using film for this exercise.
This angle was even worse. It’s a good thing I wasn’t using film for this exercise.

And sometimes it is important to think about the extra details that you can capture by changing perspectives and taking the time to figure out the best approach.

Opera House with sailboat
I had to wait a few minutes for the sailboat to get into a spot that made it look right.
Sometimes too much context is too distracting.
Sometimes too much context is too distracting and makes it hard to know what you’re supposed to look at.

But when you take the time to look at things from different angles, perspectives, and positions sometimes you get to discover something you didn’t know to ask about.

This little guy and an older buddy spend lots of time in the sun on these steps behind the opera house – I had no idea they were there until we were walking around.
This little guy and an older buddy spend lots of time in the sun on these steps behind the opera house – they are well known locally, but I had no idea they were there until we were walking around.

For me the best moments are those gems you find when you take the time to explore ideas and view points and discover something totally new. Nothing beats travel to help you remember to change your perspective now and again.

Picking tools you’ll love: don’t make yourself hate it on day one.

Every few years organizations replace a major system or two: the web site, CMS, CRM, financial databases, grant software, HR system, etc. And too often organizations try to make the new tool behave just like the old tool, and as a result hate the new tool until they realize that they misconfigured it and then spend 5-10 years dealing with problems that could have been avoided. If you’re going to spend a lot of money overhauling a mission critical tool you should love it from day one.

No one can promise you success, but I promise if you take a brand new tool and try to force it to be just like the tool you are replacing you are going to be disappointed (at best).  Salesforce is not CiviCRM, Drupal is not WordPress, Salsa is not Blackbaud. Remember you are replacing the tool for a reason, if everything about your current tool was perfect you wouldn’t be replacing it in the first place. So here are my steps for improving your chances of success:

  1. List the main functions the tool needs to accomplish: This is the most obvious thing to do, but make sure your list only covers the things you need to do, not the ways you currently do it. Try to keep yourself at a relatively high level to avoid describing what you have now as the required system.
  2. List the pros and cons of what you have: Every tool I’ve ever used had pluses and minuses. And most major internal systems have stakeholders who love and hate it – sometimes that’s the same person – make sure you capture both the good and bad to help you with your selection later.
    Develop a list of tools that are well known in the field: Not just tools you know at the start of the project. Make sure you hunt for a few that are new to you. You might think you’ve heard of them all cause you walked around the vendor hall at NTC last year, but I promise you there are more companies that picked a different conference to push their wares, and there are open source tools you might have missed too.
  3. Make sure every tool has a salesperson: Open Source tools can be overlooked because no one sells them to you, and that may mean you miss the perfect tool for your organization. So for open source even the playing field by having a salesperson, or champion, for the tool. This can be an internal person who likes learning new things, or an outside expert (usually paid but sometimes volunteer).
  4. Let the sales teams sell, but don’t trust them: Let sales people run through their presentations, because you will learn something along the way. But at some point you also need to ask them questions that force them off your script. Force a demo of a non-contrived example, or of a feature they don’t show you the first time. Make them improvise and see what happens.
  5. Talk to other users, and make sure you find one who is not happy: Sure your organization is unique but lots of other organizations have similar needs for the basic tools – unless you have a software-based mission you probably do not want an email system that’s totally different from everyone else’s. A good salesperson will have no trouble giving you a list of references of organizations who love the tool, but if you want the complete picture find someone who hates it. They might hate it for totally unfair reasons, but they will shed light on the rough edges you may encounter. Also make sure you ask the people who love it what problems they run into, remember nothing is perfect so everyone should have a complaint of some kind.
  6. Develop a change strategy: In addition to a data migration plan you need to have a plan that covers introducing the new tool to your colleagues, training the users, communicating to leadership the risks and rewards of the new setup, and setting expectations about any disruptions the change over may cause.  I’ve seen an organization spend nearly a half million dollars on customization of a complex toolset only to have the launch fail because they didn’t make sure the staff understood that the new tool would change their day-to-day tasks.
  7. Develop a migration plan: Plan out the migration of all data, features, and functions as soon as you have your new tool selected. This is not the same thing as your change strategy, this is nuts and bolts of how things will work. Do not attempt to do this without an expert. You made yourself an expert in the field, but not of every in-and-out of the new system: hire someone who is.  That could be a setup team from the company that makes it, a 3rd party consultant, or a new internal staff person who has experience with different instances of the tool.
  8. Get staff trained on using the new tool: don’t scrimp on staff training. Make sure they have a chance to learn how to do the things they will actually be doing on a day-to-day basis.  If you can afford to have customized training arranged I highly recommend it, if you cannot have an outside person do it, consider custom building a training for your low-level internal users yourself.
  9. Develop a plan for ongoing improvement: you will not be 100% happy 100% of the time, and over time those problems will get worse as your needs shift. So make sure you are planning to consistently improve your setup. That can take many forms and what makes the most sense will vary from tool to tool and org to org, but it probably will mean a budget so ask for money from the start and build it into your ongoing budget for the project. Plan for constant improvement or you will find a growing list of pain points that push you to redo all this work sooner than expected.You’ll notice I never actually told you to make your choice. Once you’ve completed steps 1-6 you probably will see an obvious choice, of not: guess. You have a list, you listened to 20 boring sales presentations, you’ve read blogs posts, white papers, and ad materials. You now are an expert on the market and the tools. If you can’t make a good pick for your organization, no one else can either so push aside your imposter syndrome and go with your gut. Sure you could be wrong, but do the best you can and move forward. It’s usually better to make a choice than waffle indefinitely.

Sins Against Drupal 2

This is part of my ongoing series about ways Drupal can be badly misused. These examples are from times someone tried to solve an otherwise interesting problem in just about the worst possible way.

I present these at SC Drupal Users Group meetings from time to time as an entertaining way to discuss interesting problems and ways we can all improve.

This one was presented about a year ago now (August 2015). Since I wasn’t working with Drupal 8 when I did this presentation the solution here is Drupal 7 (if someone asks I could rewrite for Drupal 8).


The Problem

The developer needed to support existing Flash training games used internally by the client. Drupal was used to provide the user accounts, game state data, and exports for reporting. The games were therefore able to authenticate with Drupal and save data to custom tables in the main Drupal database. The client was looking for some extensions to support new variations of the games and while reviewing the existing setup I noticed major flaws.

 

The Sinful Solution

Create a series of bootstrap scripts to handle all the interactions, turning Drupal into a glorified database layer (also while you’re at it, bypass all SQL injection attack protections to make sure Drupal provides as little value as possible).

The Code

There was a day when bootstrap scripts with a really cool way to do basic task with Drupal. If you’ve never seen or written one: basically you load bootstrap.inc, call drupal_bootstrap() and then write code that takes advantage of basic Drupal functions – in a world without drush this was really useful for a variety of basic tasks. This was outmoded (a long time ago) by drush, migrate, feeds, and a dozen other tools. But in this case I found the developer had created a series of scripts, two for each game, that were really similar, and really really dangerous. The first (an anonymized is version shown below) handled user authentication and initial game state data, and the second allowed the game to save state data back to the database.

As always the script here was modified to protect the guilty, and I should note that this is no longer the production code (but it was):

<?php 
require_once './includes/bootstrap.inc'; 
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); // "boot" Drupal 
define("KEY", "ed8f5b8efd2a90c37e0b8aac33897cc5"); // set key 

// check data 
if(!(isset($_POST['hash'])) || !(isset($_POST['username'])) || !(isset($_POST['password']))) { 
  header('HTTP/1.1 404'); 
  echo "status=100"; 
  exit; // missing a value, force quit 
} 

// capture data 
$hash = $_POST['hash']; 
$username = $_POST['username']; 
$password = $_POST['password']; 

// check hash validity 
$generate_hash = md5(KEY.$username.$password); 
if($generate_hash != $hash) {
  header('HTTP/1.1 404'); 
  echo "status=101"; 
  exit; // hash is wrong, force quit 
} 

// look for username + password combo 
$flashuid = 0; 
$query = db_query("SELECT * FROM {users} WHERE name = '$username' AND pass = '$password'"); 
if ($obj = db_fetch_object($query)){ 
  $flashuid = $obj->uid;
}

if($flashuid == 0) {
  header('HTTP/1.1 404');
  echo "status=102";
  exit; // no match found
}

// get user game information
$gamequery = db_query("SELECT * FROM {table_with_data_for_flash_objects} WHERE uid = '$flashuid' ORDER BY lastupdate DESC LIMIT 1");

if ($game = db_fetch_object($gamequery)){
  $time = $game->time;
  $round = $game->round;
  $winnings = $game->winnings;
  $upgrades = $game->upgrades;
} else {

  // no entry, create one in db
  $time = $round = $game_winnings = $long_term_savings = $bonus_list = "0";
  $upgrades = "";
  $insert = db_query("INSERT INTO {table_with_data_for_flash_objects} (uid, lastupdate) VALUES ('$flashuid',NOW())");
}

$points = userpoints_get_current_points($flashuid);

// echo success and values
header('HTTP/1.1 201');
echo "user_id=$flashuid&points=$points&ime=$time&round=$round&winnings=$winnings&upgrades=$upgrades";

?>

Why is this so bad?

It’s almost hard to know where to be begin on this one, so we’ll start at the beginning.

  • Bootstrap scripts are not longer needed and should never have been used for anything other than a data import or some other ONE TIME task.
  • That key defined in line 3, that’s used to track sessions (see lines 20-21). If you find yourself having to recreate a session handler with a fixed value, you should assume you’re doing something wrong. This is a solved problem, if you are re-solving it you better be sure you know more than everyone else first.
  • Error handling is done inline with a series of random error status codes that are printed on a 404 response (and the flash apps ignored all errors). If you are going to provide an error response you should log it for debugging the system, and you should use existing standards whenever possible. In this case 403 Not Authorized is a far better response when someone fails to authenticate.
  • Lines 15-17, and then line 30: a classic bobby tables SQL Injection vulnerability. Say goodbye to security from here on in. They go on to repeat this mistake several more times.
  • Finally, just to add insult to injury, the developer spends a huge amount of time copying variables around to change their name: $password = $_POST[‘password’]; $round = $game->round; There is nothing wrong just using fields on a standard object, and while there is something wrong with just using a value from $_POST, copying it to a new variable does not make it trustworthy.

Better Solutions

There are several including:

  • Use a custom menu to define paths, and have the application just go there instead.
  • Use Services module: https://www.drupal.org/project/services
  • Hire a call center to ask all your users for their data…

If I were starting something like this from scratch in D7 I would start with services and in D8 I’d start with the built-in support for RESTful web services. Given the actual details of the situation (a pre-existing flash application that you have limited ability to change) I would go with the custom router so you can work around some of the bad design of the application.

In our module’s .module file we start by defining two new menu callbacks:

function hook_menu() {

  $items['games/auth'] = array(
    'title' => 'Games Authorization',
    'page callback' => 'game_module_auth_user',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );  
  $items['games/game_name/data'] = array( // yes, you could make that a variable instead of hard code
    'title' => 'Game Data',
    'page callback' => 'game_module_game_name_capture_data', // and if you did you could use one function and pass that variable
    'access arguments' => array('player'),
    'type' => MENU_CALLBACK,
);

return $items;
}

The first allows for remote authentication, and the second is an endpoint to capture data. In this case the name of the game is hard coded, but as noted in the comments in the code you could make that a variable.

In the original example the data was stored in a custom table for each game, but never accessed in Drupal itself. The table was not setup with a hook_install() nor did they need the data normalized since its all just pass-through. In my solution I switch to using hook_install() to add a schema that stores all the data as a blob. There are tradeoffs here, but this is a clean simple solution:

...
'fields' => array(
'recordID' => array(
'description' => 'The primary identifier for a record.',
'type' => 'serial',

...
'uid' => array(
'description' => 'The user ID.',
'type' => 'int',

...
'game' => array(
'description' => 'The game name',
'type' => 'text',

...
'data' => array(
'description' =>'Serialized data from Game application',
'type' => 'blob',

...

You could also take this one step further and make each game an entity and customize the fields, but that’s a great deal more work that the client would not have supported.

The final step is to define the callbacks used by the menu items in hook_menu():

function game_module_auth_user($user_name = '', $pass = '') { // Here I am using GET, but I don’t have to

  global $user;
  if($user->uid != 0) { // They are logged in already, so reject them
    drupal_access_denied();
  }

  $account = user_authenticate($user_name, $pass);

  //Generate a response based on result....
}

function game_module_[game_name]_capture_data() {
  global $user;
  if($user->uid == 0) { // They aren’t logged in, so they can’t save data
    drupal_access_denied();
  }

  $record = drupal_get_query_parameters($query = $_POST); // ← we can work with POST just as well as GET if we ask Drupal to look in the right place.

  db_insert('game_data')
    ->fields(array(
      'uid' => $user->uid, 
      'game' => '[game_name]',
      'data' => serialize($record),
     ))
    ->execute();
  // Provide useful response.
}

For game_module_auth_user() I use a GET request (mostly because I wanted to show I could use either). We get the username and password, have Drupal authenticate them, and move on; I let Drupal handle the complexity.

The capture data callback does pull directly from the $_POST array, but since I don’t care about the content and I’m using a parameterized query I can safely just pass the information through. drupal_get_query_parameters() is a useful function that often gets ignored in favor of more complex solutions.

So What Happened?

The client had limited budget and this was a Drupal 6 site so we did the fastest work we could. I rewrote the existing code to avoid the SQL Injection attacks, moved them to SSL, and did a little other tightening, but the bootstrap scripts remained in place. We then went our separate ways since we did not want to be responsible for supporting such a scary set up, and they didn’t want to fund an upgrade. My understanding is they heard similar feedback from other vendors and eventually began the process of upgrade. You can’t win them all, even when you’re right.

Share your sins

I’m always looking for new material to include in this series. If you would like to submit a problem with a terrible solution, please remove any personally identifying information about the developer or where the code is running (the goal is not to embarrass individuals), post them as a gist (or a similar public code sharing tool), and leave me a comment here about the problem with a link to the code. I’ll do my best to come up with a reasonable solution and share it with SC DUG and then here. I’m presenting next month so if you have something we want me to look at you should share it soon.

If there are security issues in the code you want to share, please report those to the site owner before you tell anyone else so they can fix it. And please make sure no one could get from the code back to the site in case they ignore your advice.