Thursday, December 13, 2012

Interact Intranet: Automate the Extraction of Binary Profile Pictures for use in Active Directory

Active Directory can be used as a central repository for the storage of profile pictures.  This source can then feed email, CRM, intranets, messaging and other enterprise software.

Unfortunately, there isn't an out of the box way for users to add these photos directly to Active Directory (as far as I'm aware!) so some trickery needs to happen to get this all to work.

This blog post will explain how we extract user profile pictures from our Interact Intranet environment and then upload them to Active Directory.


Step 1: SQL query to identify users and their pictures

Microsoft recommends square profile pictures with dimensions of 96x96.  Interact conveniently already thumbnails pictures into preset sizes.  After testing, we found that their 4th size, 75px width, has sufficient clarity when used in Active Directory, which allows us to export pictures without the need for programatic image manipulation.  Note: If you do want to investigate image manipulation you might start with a library like ImageMagick.
select PERSON.NTUsername, ASSET_INSTANCE.BinaryData
from PERSON
INNER JOIN ASSET_INSTANCE
on PERSON.AssetID=ASSET_INSTANCE.AssetID
Where PERSON.NTUsername != 'ARCHIVED'
and PERSON.AssetID is not NULL
and AssetSize='4' 

Step 2: Export pictures to a file path

Below is the code that is triggered though a nightly job to extract the binary data from the database and convert it into files at the designed path.
Credit for this code, and pulling this project together, comes from coding ninja extraordinaire Matt Chiste from Integryst who was able to complete the development in less time than it took me to write the requirements!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Data;
using System.IO;
namespace PicExporter
{
    class Program
    {
        static string connString = "Data Source=<redacted>;Initial Catalog=<redacted>;Persist Security Info=True;User ID=<redacted>;Password=<redacted>";
        static string cmdString = "select PERSON.NTUsername, ASSET_INSTANCE.BinaryData from PERSON INNER JOIN ASSET_INSTANCE on PERSON.AssetID=ASSET_INSTANCE.AssetID Where PERSON.NTUsername != 'ARCHIVED' and PERSON.AssetID is not NULL and AssetSize='4'";
        static string folder = "pics/";
        static string FILENAME_EXTENSION = ".jpg";
        static int FILENAME_INDEX = 0;
        static int BINARY_INDEX = 1;
        static string PROP_FILE = "picexporter.properties";
        static void Main(string[] args)
        {
            Console.Out.WriteLine("START: " + DateTime.Now.ToString());
            // vars
            string filename;
            loadVars();
            // set up the connection
            SqlConnection conn = new SqlConnection(connString);
            SqlCommand cmd = new SqlCommand();
            SqlDataReader rdr=null;
            try
            {
                // run the command
                conn.Open();
                cmd.Connection = conn;
                cmd.Parameters.Clear();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = cmdString;
                rdr = cmd.ExecuteReader();
                // iterate the users
                while (rdr.Read())
                {
                    // write to the log
                    Console.Out.Write("Extracting user: " + rdr[FILENAME_INDEX].ToString());
                    // get the byte array for the image
                    Byte[] b = new Byte[(rdr.GetBytes(BINARY_INDEX, 0, null, 0, int.MaxValue))];
                    rdr.GetBytes(BINARY_INDEX, 0, b, 0, b.Length);
                    // write rest of line to the log
                    Console.Out.Write(" (" + b.Length + "bytes)");
                    // check existing file
                    filename = folder + rdr[FILENAME_INDEX].ToString() + FILENAME_EXTENSION;
                    if (File.Exists(filename))
                        Console.Out.Write(" [UPDATE]" + Environment.NewLine);
                    else
                        Console.Out.Write(" [NEW]" + Environment.NewLine);
                    // user names prefixed with the domain will have a \ in them.  Need to make sure the full folder path is created
                    if (!Directory.Exists(Path.GetDirectoryName(filename)))
                    {
                        Console.WriteLine("Creating Folder: " + Path.GetDirectoryName(filename));
                        Directory.CreateDirectory(Path.GetDirectoryName(filename));
                    }
                    // write the file
                    FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write);
                    fs.Write(b, 0, b.Length);
                    fs.Close();
                }
            }
            catch (Exception ex)
            {
                Console.Out.WriteLine("EXCEPTION: " + ex.Message);
                Console.Error.WriteLine("EXCEPTION: " + ex.Message);
            }
            finally {
                // close the connections
                if (rdr != null)
                    rdr.Close();
                if (conn != null)
                    conn.Close();
            }
            Console.Out.WriteLine("FINISH: " + DateTime.Now.ToString());
        }
        static void loadVars()
        {
            string propFile = Directory.GetCurrentDirectory() + "\\" + PROP_FILE;
            string line, name, value;
            StreamReader file = null;
            // Read the file line by line
            try
            {
                file = new System.IO.StreamReader(propFile);
                while ((line = file.ReadLine()) != null)
                {
                    try
                    {
                        name = (line.Substring(0, line.IndexOf('=')));
                        value = (line.Substring(line.IndexOf('=')+1));
                        if (name == "connString")
                            connString = value;
                        else if (name == "cmdString")
                            cmdString = value;
                        else if (name == "folder")
                        {
                            folder = value;
                            if (!Directory.Exists(folder))
                            {
                                Console.WriteLine("Creating Folder: " + folder);
                                Directory.CreateDirectory(folder);
                            }
                        }
//                        else
//                            Console.WriteLine("ignoring property: name: " + name + ", val: " + value);
                    }
                    catch (Exception)
                    {
                        // ignore line without "="
//                        Console.WriteLine("ignoring line: " + line);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.Out.WriteLine("ERROR READING " + propFile + ": " + ex.Message);
                Console.Error.WriteLine("ERROR READING " + propFile + ": " + ex.Message);
            }
            finally
            {
                if (file != null)
                    file.Close();
            }
        }
    }
}
Reference: http://support.microsoft.com/kb/317016

Step 3: Import pictures to Active Directory

The following powershell script runs through a scheduled job.  The job searches the folder we have extracted photos to for newly modified photos and then calls the script, which matches the filename of the photo to the network User Name and then uses native Active Directory APIs to add the image to the user’s profile.
param($Identity,$Path);
# Import a .jpg into Active Directory for use as the Exchange/Outlook GAL Photo
# For best resultsL <10kb files, 96x96 dimensions
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin
if (!$Identity)
{
    throw "Identity Missing";
}
if (!$Path)
{
    throw "Path Missing";
}
if (!(Get-Command Get-User))
{
    throw "Exchange Management Shell not loaded";
}
$User = Get-User $Identity -ErrorAction SilentlyContinue
if (!$User)
{
    throw "User $($Identity) not found";
}
if (!(Test-Path -Path $Path))
{
    throw "File $($Path) not found";
}
$FileData = [Byte[]]$(Get-Content -Path $Path -Encoding Byte -ReadCount 0);
if($FileData.Count -gt 10240)
{
    throw "File size must be less than 10K";
}
$adsiUser = [ADSI]"LDAP://$($User.OriginatingServer)/$($User.DistinguishedName)";
$adsiUser.Put("thumbnailPhoto",$FileData);
$adsiUser.SetInfo()

Tuesday, December 4, 2012

Hide Content Based Upon Iframe Parent

Some web sites use iframes to display information from other web servers.  This is a practice that is generally frowned upon yet is a necessary evil sometimes.

I work on an employee intranet where we need to display content from another internal server.  The problem is that sometimes our employees access the site from off of our domain where they don't have access to the internal server.  To get around this issue I've created an extremely simple javascript snippet which can be added to the page to hide the content and display a message to the user explaining why they can't see the content.

I'm posting it here as a reference for myself and in case someone out there needs something like this!


<div id="whatever">Your content</div>

<STYLE type=text/css>
   .hidden { display: none; }
</STYLE>

<script type="text/javascript">

   function referrerCheck()
   {
      var mapReferrer = document.referrer;
      var mapDiv = document.getElementById('content');

      if(mapReferrer=="http://this is the URL calling the iframe")
      {
         mapDiv.className='unhidden';
      }else{
         mapDiv.className='hidden';
      }
   }

   referrerCheck();

</script>

How it works:
  1. All of this code goes onto the page which calls the iframe.
  2. The div at the top is where your iframe goes - what you want to hide if folks are off the domain.
  3. The style section sets the stage for what you want to happen if folks are off the domain.  In this case we want to not display the entire div.
  4. The javascript runs the referrerCheck() function which grabs the document.referrer which is the page that calls the iframe.
  5. The getElementById functions gets the div object so that its class can be manipulated.
  6. The If condition then determines whether or not the referrer is on or off domain and sets the class accordingly.