Chrome – Upgrade Supervised User to Normal

I recently created a protected user in chrome without really knowing anything about this feature. Later I realised I couldn’t install add ons so wanted to upgrade the users permissions and keep all the cookies and settings for this new user. There is no direct way to upgrade a user in the UI so you have to fiddle about with the filesystem, Here’s how to do it. This is for mac but should translate to other OS’s if you change the profile location.

1 Create a new user, give it a new name, this will be your upgraded users profile.
2 Close Chrome
3 Go to ~/Library/Application Support/Google/Chrome/ and work out which folder represents the old Supervised User, Copy all contents in here to the folder of the New user created in step 1*.
4 In the new user’s profile location open the file ‘Preferences’ in a text editor.
5 Find the key ‘profile’ and under here zero out the managed_user_id key so it reads – “managed_user_id”: “”,
6 completely remove the key also under profile, this looks something like this..


"managed": {
         "custodian_email": "<YOUR EMAIL>",
         "custodian_name": "<YOUR NAME>",
         "shared_settings": {
            "<YOUR CODE>=": {
               "chrome-avatar-index": {
                  "acknowledged": true,
                  "value": 13
               }
            }
         }

7 Still in the new users profile folder, remove the file ‘Managed Mode Settings’
8 Restart Chrome, your user will now be a normal user with all the old supervised user’s settings

* To work out which folder belongs to which user open the ‘Preferences’ file in a folder and search for the ‘profile’ key. Note the new user should be a folder called Profile X where X is an incremental number so you’d look at the highest.

Posted in Uncategorized | Leave a comment

Different Colours per Site Section in SASS (SCSS)

Sometimes you may need a different colour scheme for every section / primary category in your site. This can typically be a couple of hues of a colour per section, just to give a different personality to that corner of the site. For example at The Times each main section has a dominant colour which is used in typgraphy and block headings within that section. Here we took a basic approach of having CMS driven section variables which resolved to class names such as “bg-007469”, you will see these peppered around the markup, mischievously binding presentation information to the markup. Each page then has a dynamic <style> tag written out by the back end to marry things up.

Unfortunately this was built way before pre-processed CSS took off. Nowadays we have tools such as LESS & SASS to make structuring site colour more elegantly. With the use of mixins we can nominate which elements should inherit section colours all within our SCSS codebase. Here’s how..

1st in your mixins location add this.

$primaryColors: (home #E73159)
                (section2 #B0C038)
                (section3 #FDAD00)
                (anothersection #CB8840)
                (keepaddingasmany #EAA3C5)
                (sectionsasyouneed #8FBDCB);
@mixin section-color($properties:(background-color),$context:"body"){
  $postContext:"&";
  @if ($context=="&") {
    $postContext:"";
  }
  @each $i in $primaryColors {
    #{$context}.#{nth($i,1)} #{$postContext} {
      @each $property in $properties {
        #{$property}:#{nth($i,2)};
      }
    }
  }
}

$primaryColors is an array or ‘collection’ in sass language, fill this with all your sections’ colours. I’m using the nth sass function to treat the collection as a multi dimensional array. In SASS 3.3.0 there are ‘maps’ which will probably be a bit cleaner. To make this work you will need to ensure your CMS writes out the section name onto the body class. You can now call…

.content{
    @include section-color();
}

on each selector you wish to inherit a section colour. By default it will apply the colour to the background, but you can easily override this by passing a different CSS property or collection of properties e.g.

h1{
@include section-color((color,border-color));
}

(note double bracketing needed for a collection). The mixin iterates through all given properties and all possible sections and generates such CSS as…

body.home h1 {
  color: #e73159;
  border-color: #e73159;
}
body.section2 h1 {
  color: #b0c038;
  border-color: #b0c038;
}
body.section3 h1 {
  color: #fdad00;
  border-color: #fdad00;
}
body.anothersection h1 {
  color: #cb8840;
  border-color: #cb8840;
}
body.keepaddingasmany h1 {
  color: #eaa3c5;
  border-color: #eaa3c5;
}
body.sectionsasyouneed h1 {
  color: #8fbdcb;
  border-color: #8fbdcb;
}

You may have noticed this bit of conditional logic at the top:

  $postContext:"&";
  @if ($context=="&") {
    $postContext:"";
  }

This is to limit the trickery to a specific part of the site, for example the site nav. Here you may wish to render all section colours, and more than likely have your cms output a section classname on each nav entity. To switch into this mode there is a context parameter which by default is set to ‘body’. When you override this to “&” the rule will no longer use the body class prefix and instead use the normal context of the rule that you are in. So for example on your Nav elements you could call..

nav{
  li{
    @include section-color($context:"&");
  } 
}

Which compiles to

nav li.home {
  background-color: #e73159;
}
nav li.section2 {
  background-color: #b0c038;
}
nav li.section3 {
  background-color: #fdad00;
}
nav li.anothersection {
  background-color: #cb8840;
}
nav li.keepaddingasmany {
  background-color: #eaa3c5;
}
nav li.sectionsasyouneed {
  background-color: #8fbdcb;
}

Notice there is no longer a body prefix.

This all sounds a bit confusing so to help visualise what I’m talking about I’ve made a codepen.

Posted in Uncategorized | Tagged , | Leave a comment

What is a Caracal?

caracals

A recent Facebook post from IFLS, got people interested in Caracals, the wild cat I named my company after. I am often ask to pronounce the name and explain what they are. Hopefully this picture will help a little.

Posted in Uncategorized | Leave a comment

jquery.domChanged

I’ve just uploaded a new jquery plugin to github.

It’s a little plugin that allows you to bind a ‘domChanged’ event on any element. Works cross browser by using ‘MutationObserver’ for modern browsers and a polling mechanism for older browsers (IE). If you completely control the DOM on your page then this probably would be of no use. It’s only really useful if you know a 3rd party script (an ad maybe) is going to change a part of your page but you have no means of knowing when it will have finished.

Posted in Uncategorized | Leave a comment

Grunt as a file watcher in PHP/Webstorm

Why would you want to use PHPStorm’s file watcher when Grunt has a perfectly capable watching service?

If you work with many projects at any one time, I think it’s important to be able to quickly jump from one project to another without having to remember to fire a run command in a terminal screen. With PHPStorm’s filewatchers I only have to worry about one app, my IDE – PHPStorm. Of course it is possible to compile stylesheets & javascript with PHPStorm directly as described here, but I prefer Grunt as it gives you a framework for defining production build patterns along side your dev building. So to plug your Grunt workflow into PHPStorm, first define your Grunt task without using the grunt watcher like so.

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        compass: {
            dev: {
                options: {
                    require: 'susy',
                    sassDir: '../sass/',
                    cssDir: '../web/css/src',
                    outputStyle: 'expanded'
                }
            },
            prod:{
                options: {
                    require: 'susy',
                    sassDir: '../sass/',
                    cssDir: '../web/css',
                    outputStyle: 'compressed'
                }
            }
        },
        concat: {
            prod:{
                options: {
                    separator: ';'
                },
                src: ['../web/js/src/angular/*/*.js','../web/js/src/lib/*.js','../web/js/src/*.js'],
                dest: '../js/min/concat.js'

            }
        },
        uglify: {
            prod:{
                files: {
                    '../web/js/min/scripts.min.js': ['<%= concat.prod.dest %>']
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-concat-css');
    grunt.loadNpmTasks('grunt-contrib-compass');
    grunt.registerTask('default', [ 'compass:dev']);
    grunt.registerTask('prod', [ 'compass:prod','concat:prod','uglify:prod']);
};

Now running ‘grunt’ in my grunt directory will run the default task and I can use ‘grunt prod’ to build for production. Make sure grunt alone works at this point.

Next I need to tell PHPStorm to run ‘grunt’ every time I edit an SCSS file. First double check where grunt is.

$ which grunt
/usr/local/bin/grunt

Now enter this into a new file watcher form.
Screen Shot 2014-04-17 at 12.32.28
Make sure you set the working directory to where your grunt file is. Now everytime you save an SCSS file your grunt task will execute.

Posted in Uncategorized | 1 Comment

Gmaps2BirdsEye – Google maps to Birds Eye View Bookmarklet

Screen Shot 2014-03-26 at 10.28.43

Google Maps pretty much wins the map wars over Bing Maps hands down in my opinion, except for one killer Bing feature… Bird’s Eye!

captain_birdseye

Bird’s Eye really does come into its own when you’re looking at property to rent or buy, as it allows you to see a street from different angles for a unique perspective of the local topography. I made this bookmarklet to quickly switch between views of a location; so whether you want street, satellite or bird’s eye view, you can switch quickly with this bookmarket. (What’s a bookmarklet?).

To use

Drag this link to your bookmark bar.
Gmaps2BirdsEye

Locate a place of interest in Google Maps, click the bookmarklet to switch between Google Maps & Bing Bird’s eye.

So far tested this on Chrome and Firefox. Note if you fire the bookmarklet when you’re not on Google Maps it will go to Google Maps.

Here is the code..

$j = jQuery.noConflict();

if(isGmaps()){
    l=getLoc("g");
    location.href="http://www.bing.com/maps/default.aspx?sty=b&cp=" + l.lat + "~" + l.long +"&lvl=" + l.zoom;
}else if(isBing()){
    l=getLoc("b");
    location.href="https://www.google.co.uk/maps/@" + l.lat+"," + l.long+ "," + l.zoom+ "z";
}else{
    location.href="http://maps.google.com"
}
function isGmaps(){
    return(location.host.search(/^maps.google/)==0||location.href.search(/^https?:\/\/www.google.co.*\/maps/)==0)
}
function isBing(){
    return(location.href.search(/^https?:\/\/www.bing.co.*\/maps/)==0)
}
function getLoc(mode){
    if (mode=="g"){
        if($j(".permalink-button").length>0){
            q=parseQuery($j(".permalink-button").attr("href"));
            ll = q["ll"].split(",");
            return {
                lat:ll[0],
                long:ll[1],
                zoom:q["z"]
            }
        }else{
            a1 = location.href.split("@").pop();
            a2= a1.split(",");
            return{
                lat:a2[0],
                long:a2[1],
                zoom:parseInt(a2[2])
            }
        }
    }else{
        q=parseQuery(VEShell.API.shareLink());
        if (q["cp"].indexOf("~")<0){
            VEShell.API.setMapStyle("r");
            q=parseQuery(VEShell.API.shareLink());
        }
        ll=q["cp"].split("~");
        return{
            lat:ll[0],
            long:ll[1],
            zoom:q["lvl"]
        }

    }
}
function parseQuery(str){
    if(typeof str != "string" || str.length == 0) return {};
    var s = str.split("&");
    var s_length = s.length;
    var bit, query = {}, first, second;
    for(var i = 0; i < s_length; i++)
    {
        bit = s[i].split("=");
        first = decodeURIComponent(bit[0]);
        if(first.length == 0) continue;
        second = decodeURIComponent(bit[1]);
        if(typeof query[first] == "undefined") query[first] = second;
        else if(query[first] instanceof Array) query[first].push(second);
        else query[first] = [query[first], second];
    }
    return query;
}

Posted in Uncategorized | Tagged | Leave a comment

IE8 Developer Tools Bug: Attaching stylesheets

It seems if you attach a stylesheet with code like this…

$("head").append('<link type="text/css" href="/assets/css/tmp.css" rel="stylesheet" />');

You may not see all the styles in the right hand panel here. ↴

Screen Shot 2014-02-28 at 12.12.20
Although the browser will render any styles in tmp.css, IE developer tools will not be aware of that sheet in it’s style view. So if you have a lot of CSS debugging to do in IE8 (you will) and you wish to attach a sheet dynamically with javascript, I suggest you just hard code the link tag in whilst you’re developing/fixing bugs, then revert to the JS appending when done.

Posted in Development | Tagged , , | Leave a comment

Handling Retina Images in a CMS

There are a few ways to handle the displaying of retina images such as sending a more compressed double resolution to all clients or a more webserver-based solution.

Both are great, but in a recent project for womenshealth.co.uk, I was using a CMS – Expression Engine and needed a different approach. .

Like most CMSs, Expression Engine generates a series of crops from one large ‘master’ file. So rather than one image and it’s double size retina counterpart, we have to think in terms of many file crops and the context of the image.

To do this, I wrote a JavaScript that, asks each image what size it wants to be and what pixel ratio the client is. Using this information, and having knowledge of all the different crops available on the server, we should be able to identify the perfect crop to fetch.

The script became even more useful because womanshealth.co.uk is a responsive site; so images need to be of varying size depending on the user’s browser window. I eventually added some re-size detection, so even if a browser window started tiny, forcing a thumbnail image to download, you wouldn’t be stuck with a nasty pixelated stretched thumbnail when the window is maximised.

A typical URL from the Expression Engine image plugin we used would look like this…

/images/423/buccament_bay_resort_st_vincent_caribbean__small_4x3.jpg

There is some specific code in my script to parse this pattern; you’ll have to amend this to match your CMS. Also, a crop object is defined (crops) – make sure you change that to match the crops you’re using in your CMS.

To work the script runs at the bottom of the page just before . There’s also an inline style in the head to temporarily prevent the natural image sizes loading, this will only kick in with js (using a modernizr selector), it is then removed when the script is done doing it’s stuff. Note I’ve given all images I want to process a class of “c” for “content”. UI images won’t be touched by this.

This is the inline style…

    <style type="text/css" id="retina-hider">
        .js img.c {display:none}
    </style>

Here’s the script with plenty of inline commentary…

(function(){
    //this little generic function enables us to have a sort of 'after resize' event ie we don't want to fire
    //loads of ret checks as we're resizing
    var delay = (function(){
        var timer = 0;
        return function(callback, ms){
            clearTimeout (timer);
            timer = setTimeout(callback, ms);
        };
    })();


    function ret(firstRun){
        //process retina images
        var dpr = 1, crop,currentCrop,bestCrop,aSplits;
        if(window.devicePixelRatio !== undefined) dpr = window.devicePixelRatio;
        //define crop object
        var crops = {thumb:152,small:320,medium:640,big:1112};

        $("img.c").each(function(i){
            var $img = $(this);
            //calculate image crop from filename
            //We're gonna be counting 2 _'s from the end of the filename to get the crop name, this bit is specific to our CMS
            aSplits = $img.attr("src").split("_");
            currentCrop =  aSplits[aSplits.length-2];
            for(crop in crops){
                if(  (crops[crop]>=$img.width()*dpr ||
                    crop=="big")
                    ){
                    if(currentCrop==crop||(crops[currentCrop]>crops[crop] && !firstRun ) ){
                        //we already have the optimal crop or the crop we have is bigger anyhow, (no need to download a smaller version) so exit this images each loop
                        //if first run it's ok to get a smaller crop because we have nothing anyhoo.
                        return true;
                    }
                    //we've found a crop that's bigger or equal, (always scale down)   or if we've reached the end (largest) and it's different crop from current one.
                    aSplits[aSplits.length-2]=crop;
                    $img.attr("src",aSplits.join("_"));
                    break;
                }
            }
        }) /**/
    }
    ret(true);
    $("#retina-hider").remove();
    //bind resize
    $(window).resize(function(){
        delay(function(){
           ret(false);
        }, 500);
    });
})()

Posted in Uncategorized | Tagged , | Leave a comment

File Watcher for compass in PHPStorm / Webstorm

I love Jetbrains PHPStorm, it’s the best IDE out there in my opinion. The latest version 6.0 released recently included “File Watchers – for easy Sass, LESS, SCSS, CoffeeScript, TypeScript transpilation”. The SCSS file watcher almost works with compass projects but when you start including like ‘@import “compass/reset”;’ you get errors because the compass/reset stylesheet isn’t actually in your project, it’s located centrally here /Library/Ruby/Gems/1.8/gems/compass-0.12.2/frameworks/compass.

So here’s how to get compass SASS files auto compiling.
Screen Shot 2013-03-22 at 13.51.08
Here is the xml config if you want to paste it into .idea/watcherTasks.xml

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectTasksOptions">
    <TaskOptions isEnabled="true">
      <option name="arguments" value="compile $FileParentDir$" />
      <option name="checkSyntaxErrors" value="true" />
      <option name="description" value="Compiles .scss files into .css files" />
      <option name="exitCodeBehavior" value="ERROR" />
      <option name="fileExtension" value="scss" />
      <option name="immediateSync" value="true" />
      <option name="name" value="Compass" />
      <option name="output" value="" />
      <option name="outputFilters">
        <array />
      </option>
      <option name="outputFromStdout" value="false" />
      <option name="passParentEnvs" value="true" />
      <option name="program" value="/usr/bin/compass" />
      <option name="scopeName" value="Project Files" />
      <option name="trackOnlyRoot" value="true" />
      <option name="workingDir" value="$Projectpath$" />
      <envs />
    </TaskOptions>
  </component>
</project>
Posted in Uncategorized | 11 Comments

Automatic Apache Virtual Hosts pt2

I’ve added the 3 following things to my Automatic Apache Virtual Host Applescript discussed (part one of this post here).

  1. A vbscript for a VMware Fusion virtual machine
  2. An index page of all virtual hosts
  3. Root directory browsing of the webserver

First the vbscript which I’ve called updateHosts.vbs, for this I’ve used these libraries.  The general idea is the ghost applescript will check is my virtual windows machine is running and if so run the vbscript inside of the virtual machine using the vmware vmrun command.  I also run the vbscript on windows vm login as well, just incase I last ran the applescript whilst the VM was off.

 

option Explicit
Dim hostip,root ,domain, clients, client, projects, project, projectSubFolders, projectSubFolder,oFSO
hostip="172.16.214.1"
root = "W:\Projects"

Dim o_h : Set o_h = New std_host_file

Call o_h.Load( "C:\Windows\System32\drivers\etc\hosts" , False )
Call o_h.DeleteHostEntry( hostip )

set oFSO = CreateObject("Scripting.FileSystemObject") 
ofso.getfolder(root)
for each client in ofso.getfolder(root).subfolders
	For each project in client.SubFolders
		If Left(project.Name,1) &lt;&gt; "_" then
			For Each projectSubFolder in project.SubFolders
				if  projectSubFolder.Name = "web" then
					Call o_h.AddHostEntry( hostip, project.name &amp; "." &amp; client.name )
				end if
			Next
		End if
	next
Next
Call o_h.Save("C:\Windows\System32\drivers\etc\hosts")

Class std_host_file
	Private Sub Class_Initialize()
		Set m_lines = CreateObject("Scripting.Dictionary")
		Set m_ip_map = CreateObject("Scripting.Dictionary")
		Set m_alias_map = CreateObject("Scripting.Dictionary")
		m_alias_map.CompareMode = vbTextCompare
	End Sub
	Private sub Class_Terminate()
		Set m_lines = Nothing
		Set m_ip_map = Nothing
		Set m_alias_map = Nothing
	End Sub
	' Function returns the ip and data
	Private Sub parse_line( ByVal line , ByRef comment , ByRef ip , ByRef aliases  ) 
		Dim rx : Set rx = New RegExp
		Dim r
		rx.Global = False
		rx.IgnoreCase = True
		rx.Pattern = "\s*(#.*)\s*"
		' Parse Comment
		If rx.Test( line ) Then
			Set r = rx.Execute( line )
			comment = r.Item(0).subMatches.Item(0)
			line = rx.Replace( line , "" )
		End If		
		rx.Pattern = "\s*((\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b)|(\s*((([0-9A-F]{1,4}:){7}([0-9A-F]{1,4}|:))|(([0-9A-F]{1,4}:){6}(:[0-9A-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){5}(((:[0-9A-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){4}(((:[0-9A-F]{1,4}){1,3})|((:[0-9A-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){3}(((:[0-9A-F]{1,4}){1,4})|((:[0-9A-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){2}(((:[0-9A-F]{1,4}){1,5})|((:[0-9A-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){1}(((:[0-9A-F]{1,4}){1,6})|((:[0-9A-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-F]{1,4}){1,7})|((:[0-9A-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*))\s*"
		' Parse IP
		If rx.Test( line ) Then
			Set r = rx.Execute( line )
			ip = r.Item(0).subMatches.Item(0)
			aliases = rx.Replace( line , "" )
		End If
	End Sub
	' Function returns the ip and data
	Private Function getip( ByRef str , ByRef ip  ) 
		getip = False 
		Dim rx_ip : Set rx_ip = New RegExp
		rx_ip.Global = False
		rx_ip.IgnoreCase = True
		rx_ip.Pattern = "\s*((\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b)|(\s*((([0-9A-F]{1,4}:){7}([0-9A-F]{1,4}|:))|(([0-9A-F]{1,4}:){6}(:[0-9A-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){5}(((:[0-9A-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){4}(((:[0-9A-F]{1,4}){1,3})|((:[0-9A-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){3}(((:[0-9A-F]{1,4}){1,4})|((:[0-9A-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){2}(((:[0-9A-F]{1,4}){1,5})|((:[0-9A-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){1}(((:[0-9A-F]{1,4}){1,6})|((:[0-9A-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-F]{1,4}){1,7})|((:[0-9A-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*))\s*"
		If rx_ip.Test( str ) Then
			Dim r : Set r = rx_ip.Execute( str )
			ip = r.Item(0).subMatches.Item(0)
			str = rx_ip.Replace( str , "" )
			getip = True
		End If
	End Function
	' Internal function used to validate IPV4 addresses
	Private Function isipv4( ByVal ip )
		Dim rx_ipv4 : Set rx_ipv4 = New RegExp
		rx_ipv4.Global = false
		rx_ipv4.IgnoreCase = True
		rx_ipv4.Pattern = "^\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b$"
		isipv4 = rx_ipv4.Test( ip )
	End Function
	' Internal function used to validate IPV6 addresses
	Private Function isipv6( ip )
		Dim rx_ipv6 : Set rx_ipv6 = New RegExp
		rx_ipv6.Global = false
		rx_ipv6.IgnoreCase = True
		rx_ipv6.Pattern = "^\s*((([0-9A-F]{1,4}:){7}([0-9A-F]{1,4}|:))|(([0-9A-F]{1,4}:){6}(:[0-9A-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){5}(((:[0-9A-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-F]{1,4}:){4}(((:[0-9A-F]{1,4}){1,3})|((:[0-9A-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){3}(((:[0-9A-F]{1,4}){1,4})|((:[0-9A-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){2}(((:[0-9A-F]{1,4}){1,5})|((:[0-9A-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-F]{1,4}:){1}(((:[0-9A-F]{1,4}){1,6})|((:[0-9A-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-F]{1,4}){1,7})|((:[0-9A-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$"			
		IsIPV6 = rx_ipv6.Test( ip )
	End Function
	' Internal function used to generate unique IDs
	Private Function genguid()
		Dim guidgen : Set guidgen = CreateObject("Scriptlet.TypeLib") 
		genguid = Mid(guidgen.Guid, 2, 36)
	End Function
	' Function returns true on success, otherwise false if the ip doesn't exist
	Public Function GetHostEntryAliases( ByVal ip , ByRef aliases )
		GetHostEntryAliases = False
		aliases = Array
		If m_ip_map.Exists( ip ) Then 
			aliases = m_lines.Item( m_ip_map.Item(ip) )(1).Keys
			GetHostEntryAliases = True
		End If
	End Function 
	' Function returns true on success, otherwise false if alias doesn't exist
	Public Function GetHostEntryAliasAddresses( ByVal alias , ByRef ips )
		GetHostEntryAliasAddresses = False
		ips = Array
		If m_alias_map.Exists( alias ) Then 
			ips = m_alias_map.Item( alias ).Keys
			GetHostEntryAliasAddresses = True
		End If
	End Function 
	' Removes a host entry by IPV4 or IPV6 address, return true
	' on success, otherwise false if the IP is doesn't exist
	Public Function DeleteHostEntry( ByVal ip )
		DeleteHostEntry = False
		If m_ip_map.Exists( ip ) Then 
			Dim a
			Dim uid : uid = m_ip_map.Item(ip)
			' Remove the ip's from the associated aliases
			For Each a In m_lines.Item(uid)(1).Keys 
				If m_alias_map.Item( a ).Exists( ip ) Then
					m_alias_map.Item( a ).Remove( ip )
				End If
				' If there are no more assoicated IPs remove the alias
				If m_alias_map.Item(a).Count = 0 Then
					m_alias_map.Remove( a )
				End If 
			Next
			' This *should* exist since we manage the entries and mappings
			m_lines.Remove( uid )
			m_ip_map.Remove( ip )
			DeleteHostEntry = True
		End If
	End Function
	' Removes a host alias by IPV4 or IPV6 address
	Public Function DeleteHostEntryAlias( ByVal ip , ByVal alias )
		DeleteHostEntryAlias = False
		' If the IP is valid
		If m_ip_map.Exists( ip ) Then
			Dim uid : uid = m_ip_map.Item(ip)		
			' If the alias exists remove the ip and if no more ip's are mapped to the alias remove the alias	
			If m_alias_map.Exists( alias ) Then
				If m_alias_map.Item( alias ).Exists( ip ) Then
					m_alias_map.Item( alias ).Remove( ip )
				End If
				If m_alias_map.Item(alias).Count = 0 Then
					m_alias_map.Remove( alias )
				End If
			End If
			' If IP no longer has aliases associated with it remove it
			Call delalias( uid , alias )
			If m_lines.Item( uid )(1).Count = 0 Then
				m_lines.Remove( uid )
				m_ip_map.Remove( ip )
			End If
			DeleteHostEntryAlias = True
		End If
	End Function
	' Adds a host entry by IPV4 or IPV6 address, alias should be 
	' the text alias for the address. Returns true on success
	Public Function AddHostEntry( ByVal ip , ByVal alias )		
			AddHostEntry = False 		
			Dim rx : Set rx = New RegExp
			rx.Global = true
			rx.IgnoreCase = True
			rx.Pattern = "\s*"
			alias = rx.Replace( alias , "" )
			ip = rx.Replace( ip , "" )
			' Validate IP
			If isipv6( ip ) Or isipv4( ip ) Then 
				' Check for alias in the alias mapping
				If Not m_alias_map.Exists( alias ) Then	Call m_alias_map.Add( alias , CreateObject("Scripting.Dictionary") )
				If Not m_alias_map.Item( alias ).Exists( ip ) Then Call m_alias_map.Item( alias ).Add( ip , "" )
				' Map IP -&gt; alias 
				If m_ip_map.Exists( ip ) Then 
					' Lookup the index by ip then add aliases
					Call addalias( m_ip_map.Item(ip) , alias )
				Else
					' Store File Line
					Dim uid : uid = genguid
					Call m_lines.Add( uid , Array( ip , CreateObject("Scripting.Dictionary") , vbNullString ) )
					Call addalias( uid , alias )
					Call m_ip_map.Add( ip , uid )
				End If
				AddHostEntry = True
			End If	
	End Function
	' Should be used for debugging the data
	Public Sub DumpData( )
		Dim id, a, ip, a_map
		' Debug dump host file out to text
		WScript.Echo( "-------------------------------------------------------------------" )
		For Each id In m_lines.Keys
			If TypeName( m_lines.Item(id) )  = "String" Then
				WScript.Echo m_lines.Item(id)
			ElseIf TypeName( m_lines.Item(id) ) = "Variant()" Then
				WScript.StdOut.Write lpad( m_lines.Item(id)(0) &amp; "" , " " , 16 )  &amp; Space(8)
				For Each a In m_lines.Item(id)(1).Keys
					WScript.StdOut.Write a &amp; " " 
				Next
					WScript.StdOut.Write m_lines.Item(id)(2) &amp; vbCrLf
			End If 
		Next
		WScript.Echo( "-------------------------------------------------------------------" )

		For Each ip In m_ip_map.Keys
			For Each a In m_lines.Item( m_ip_map.Item(ip) )(1).Keys
				WScript.Echo "IP [" &amp; ip &amp; "] ID Map {" &amp; m_ip_map.Item(ip)	&amp; "} --&gt; alias [" &amp; a &amp; "]"
				If m_alias_map.Exists( a ) Then 
					For Each a_map In m_alias_map.Item( a ).Keys
						WScript.Echo "alias Key [" &amp; a &amp; "] IP --&gt; [" &amp; a_map &amp; "]"
					Next
				End If
			Next
		Next	 
	End Sub 
	' Internal formattig function for padding host data
	Private Function lpad ( str , padch , padlen ) 
		If padlen - Len(str) &gt;= 0 Then 
			Lpad = String(padlen - Len(str),padch) &amp; str 
		Else
			Lpad = str	
		End If 	
	End Function
	' Returns all the IP addresses defined in the host file
	' Returns true on success, otherwise false
	Public Function GetAllHostEntryAddresses( ByRef ips )
		GetAllHostEntryAddresses = False
		ips = Array
		If m_ip_map.Count &gt; 0 Then 
			ips = m_ip_map.Keys
			GetAllHostEntryAddresses = True
		End If
	End Function
	' Returns all the aliases defined in the host file
	' Returns true on success, otherwise false
	Public Function GetAllHostEntryAliases( ByRef aliases )
		GetAllHostEntryAliases = False
		aliases = Array
		If m_alias_map.Count &gt; 0 Then 
			aliases = m_alias_map.Keys
			GetAllHostEntryAliases = True
		End If
	End Function

	' Write host file returns true if file could be opened for writing
	Public Function Save( ByVal hostfile )		
		On Error Resume Next
		Save = False
		Dim id, a
		Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")
		Dim oFile : Set oFile = Nothing
		Set oFile = objFSO.OpenTextFile( hostfile , 2 , True )
		If Not oFile Is Nothing Then 
			For Each id In m_lines.Keys
				If TypeName( m_lines.Item(id) ) = "String" Then
					Call oFile.WriteLine( m_lines.Item(id) )
				ElseIf TypeName( m_lines.Item(id) ) = "Variant()" Then
					Call oFile.Write( lpad( m_lines.Item(id)(0) &amp; "" , " " , 16 )  &amp; Space(8) )
					For Each a In m_lines.Item(id)(1).Keys
						Call oFile.Write( a &amp; " " )
					Next
					Call oFile.Write( m_lines.Item(id)(2) &amp; vbCrLf )
				End If 
			Next
			Save = True
		End If
		Set objFSO = Nothing
	End Function

	' Write host file returns true if file could be opened for reading
	Public Function Load( ByVal hostfile , ByVal bmergecomments )
		On Error Resume Next		
		Load = False
		m_lines.RemoveAll()
		m_ip_map.RemoveAll()
		m_alias_map.RemoveAll()
		Dim rx : Set rx = New RegExp
		rx.Global = true
		rx.IgnoreCase = True
		rx.Pattern = "\s+"
		Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")
		Dim oFile : Set oFile=Nothing
		Set oFile = objFSO.OpenTextFile( hostfile , 1 )
		If Not oFile Is Nothing Then 
		While Not oFile.AtEndOfStream
			Dim pos : pos = 0
			' Data will not be modified to preserve the file context
			Dim data : data = Trim(oFile.ReadLine())
			' Remove all the extra whitespace so we have a single spacing	
			' Line will be chopped up to see valid information exists
			Dim line : line = Trim(rx.Replace(data, " "))
			Dim ip : ip = vbNullString
			Dim comment : comment = vbNullString
			Dim aliases : aliases = vbNullString
			' Check if the line is empty
			If line &lt;&gt; vbNullString Then					
				Call parse_line( line , comment , ip , aliases )
				If isipv4( ip ) Or isipv6( ip ) Then 	
					Dim a, uid
					' Map alias -&gt; IPs
					For Each a In Split(aliases)
						If Not m_alias_map.Exists( a ) Then 
							Call m_alias_map.Add( a , CreateObject("Scripting.Dictionary") )
						End If
						If Not m_alias_map.Item( a ).Exists( ip ) Then Call m_alias_map.Item( a ).Add( ip , "" )
					Next
					' Map IP -&gt; alias 
					If m_ip_map.Exists( ip ) Then
						uid = m_ip_map.Item(ip)
						' Lookup the index by ip then add aliases
						For Each a In Split(aliases)
							Call addalias( uid , a )
						Next
						If bmergecomments Then 
							' Overkill (should be first # )
							pos = InStr( 1, comment , "#" , vbTextCompare )
							If pos &lt;&gt; 0 Then 
								' Replace leading # from dual comment
								comment = Mid( comment , pos + 1 )
								' Merge comments
								Call setcomment( uid , m_lines.Item( uid )(2) &amp; "," &amp; comment )
							End If
						End If
					Else
						' Store File Line
						uid = genguid
						Call m_lines.Add( uid , Array( ip , CreateObject("Scripting.Dictionary") , comment ) )
						For Each a In Split(aliases)
							Call addalias( uid , a )
						Next
						Call m_ip_map.Add( ip , uid )
					End If
				Else 
                                        ' Unknown IP format or malformed file
					Call m_lines.Add(genguid,data)
				End If
			Else
				Call m_lines.Add(genguid,data)
			End If
		Wend
		Load=True
		End If
		Set objFSO = Nothing
	End Function
	Private Function setip( uid , ip )
		setip = False
		If m_lines.Exists( uid ) Then
			m_lines.Item(uid) = Array( ip , m_lines.Item(uid)(1) , m_lines.Item(uid)(2) ) 
			setip = True
		End If
	End Function
	Private Function setcomment( uid , comment )
		setcomment = False
		If m_lines.Exists( uid ) Then
			Dim a : a = m_lines.Item(uid)
			m_lines.Item(uid) = Array( m_lines.Item(uid)(0) , m_lines.Item(uid)(1) , comment )  
			setcomment = True
		End If
	End Function
	Private Function addalias( uid , alias )
		addalias = False
		If m_lines.Exists( uid ) Then
			If Not m_lines.Item(uid)(1).Exists( alias ) Then 
				Call m_lines.Item(uid)(1).Add( alias , "" )
				addalias = True
			End If
		End If
	End Function
	Private Function delalias( uid , alias )
		delalias = False
		If m_lines.Exists( uid ) Then
			If m_lines.Item(uid)(1).Exists( alias ) Then 
				Call m_lines.Item(uid)(1).Remove( alias )
				delalias = CBool( Not m_lines.Item(uid)(1).Exists( alias ) )
			End If
		End If
	End Function
	Private m_lines
	Private m_ip_map
	Private m_alias_map
End Class

Note the variables you may need to tweak here are hostip and root. I run my vm in NAT network mode which gives my windows machine access to my mac via the ip 172.16.214.1. The root variable is a shared vm folder here all my projects are stored.

The next addition is a handy little index page that returns all my virtual hosts. For this I’ve added the folder templates/index/web in order for me to get a domain of http://index.templates/. In here I’ve generated a hosts.txt which is an export of all the hosts generated by ghost using the following export command:

do shell script "ghost export > '" & unixRoot & "/templates/index/web/hosts.txt'" user name theUserName password thePassword with administrator privileges

I then parse this with the following index.php:

<!DOCTYPE HTML>
<html>
<body>
<?php
$masterList = array();
foreach (file("hosts.txt") as $name) {
	//get rid of ip address part of line
	$hosts = explode(" ",$name); 
	
	//Work out top level domain
	$subs = explode(".",$hosts[0]); 
	$topLevel = $subs[count($subs)-1];
	
	//construct array of top-level and full domain name
	$masterList[] = array($topLevel,$hosts[0]); 
}

//sort by top level
foreach ($masterList as $key => $row) {
    $tops[$key]  = $row[0];
}
array_multisort($tops, SORT_DESC, $masterList);

//output html links
$header = "";
foreach ($masterList as  $row) {
	if ($header!=$row[0]){
		$header= $row[0];
		
		echo "<h2>".$header."</h2>";
		
	}
    echo "<a href='http://" .$row[1] . "'>".$row[1]. '</a><br>';
   }
?>
</body>
</html>

I now have a handy http://index.templates/index page bookmarked on both my mac and virtual windows machine.

The final addition is not that notable really, it’s just getting the normal http://localhost to work in apache so I can browse around. My virtual hosts section of httpd.conf now looks like this:

<VirtualHost *>
    VirtualDocumentRoot /Users/stu/Projects/%-1/%-2+/web
	ServerName *
</VirtualHost>
<VirtualHost *>
    ServerName localhost
   DocumentRoot /Users/stu/Projects	
</VirtualHost>

So here’s my final ghoster.scpt applescript

set root to "Macintosh HD:Users:stu:Projects"
set unixRoot to POSIX path of root
set domain to ""
--Get mac credentials from a keychain entry called MAMP
set theUserName to do shell script ("security  find-generic-password -gl MAMP | grep \"acct\" | cut -c 19-99 | sed 's/\"//g'")
set thePassword to do shell script ("security 2>&1 >/dev/null find-generic-password -gl MAMP | cut -c 11-99 | sed 's/\"//g'")
--get rid of current hosts entries in hosts, we're gonna create fresh ones
do shell script "ghost empty" user name theUserName password thePassword with administrator privileges
--add entries based of filesystem structure
tell application "Finder"
	set clients to every folder of alias root
	repeat with client in clients
		set folderName to name of client
		if first character of folderName is not "_" then
			set projects to every folder of client
			repeat with project in projects
				set projectSubFolders to every folder of project
				repeat with projectSubFolder in projectSubFolders
					if name of projectSubFolder is "web" then
						set domain to name of project & "." & name of client
						do shell script "ghost add " & domain user name theUserName password thePassword with administrator privileges
					end if
				end repeat
			end repeat
		end if
	end repeat
end tell
--export what we've just made in a text file for the index.php to parse
do shell script "ghost export > '" & unixRoot & "/templates/index/web/hosts.txt'" user name theUserName password thePassword with administrator privileges
--get credentials for the windows VM
set winUserName to do shell script ("security  find-generic-password -gl win | grep \"acct\" | cut -c 19-99 | sed 's/\"//g'")
set winPassword to do shell script ("security 2>&1 >/dev/null find-generic-password -gl win | cut -c 11-99 | sed 's/\"//g'")
--get a list of running vmware machines
set test to do shell script "'/Applications/VMware Fusion.app/Contents/Library/vmrun' list"
--see if my windows dev machine is running
set search to offset of "win.vmx" in test
if search is greater than 0 then
	--if it's running fire the vbscript inside the guest instance
	do shell script "'/Applications/VMware Fusion.app/Contents/Library/vmrun' -T fusion -gu " & winUserName & " -gp " & winPassword & " runProgramInGuest '/pathtovmshere/win.vmx' -interactive 'c:\\windows\\system32\\cscript.exe' 'c:\updateHosts.vbs'"
end if

Posted in Uncategorized | Tagged , , , | Leave a comment