Category Archives: Examples

Examples of school-related projects

Release: New Tab Redirect 3.0

On Wednesday, I began the rollout of New Tab Redirect 3.0 with some bug fixes and a huge new feature: a built in ‘Apps’ page.

Preface for Searchers

By 9pm of release day for New Tab Redirect 3.0, I lost all faith in humanity. I’m a professional software engineer, so I realize users want what they want and if you can’t or don’t give it to them they get really bitchy. But, New Tab Redirect is free software so I technically have nobody to answer to. I’m a human and I make mistakes. I rolled out to 50% of users and forgot to add an explanation of the permissions changes to the description on the web store and to the wiki. Luckily, one user submitted an issue and another emailed me. That’s two users out of 750,000/2 users.

For anyone looking for an explanation of the new permissions, I’ve explained it on the wiki. If you read the explanation and still don’t agree with the added permissions, use another extension… it’s that simple. Don’t be a jerk and call me names or suggest that I’m doing something illegal with no grounds for such defamation. I have no respect for people who intentionally hurt others. One person went as far as to say I had a huge ego; really… I don’t see how giving people something for free and continuously improving and maintaining it for 5 years means I have a huge ego. But, whatever. The rest of this post is about some awesome technical stuff in the new version, namely the New Tab Redirect ‘Apps’ page.

Why a new version?

When Chrome 33 removed chrome-internal://newtab (which pointed to the ‘good’ New Tab Page), I realized that I could create something that I actually wanted personally, while at the same time fixing the whole ‘address bar does not focus’ problem all users targeting chrome://apps were having. Because New Tab Redirect actually redirects to the user’s optional URL, the focus that Chrome gives to New Tab override pages gets lost. That’s just how it is. I’ve documented workarounds on the extension’s wiki.

Another reason for the new version is because Google is cracking down on extensions. Extensions must now have a single visible UI. In other words, although New Tab Redirect 2.2 opened a welcome page and provided an options page, there was no user interface. It was a ‘New Tab Override’ page that didn’t directly offer an override of the new tab page. In other words, there was no default override on installation. I fixed that by creating the override page I wanted by default (I used chrome-internal://newtab). Doing this also meant I would get 5-10 less emails each week from users saying “I use chrome://apps, but I wish the address bar would focus so I could search just like the old new tab page”. Done and done.

Technology: AngularJS

At work, we’ve been using AngularJS on a new project. I’m really loving it. I decided to use AngularJS for the New Tab Redirect ‘Apps’ page because it would mean minimal, reusable code with a clean structure that my users could read and understand.

AngularJS is really web development for engineers. Normal JavaScript is often written with all kinds of haphazardly-structure files, crazy include structures, and continuation passing style causing ridiculously nested functions and hard to read code. AngularJS offers a clean modular structure with a service locator, dependency injection, and the ability to declaratively extend HTML.

The main parts of AngularJS are:

Services

Singletons that provide some shared functionality. A Singleton means there will be one instance of an object in the application a time. An interesting thing about services in AngularJS is that you can actually create factories with static data via services.

Factories

Factories are like services, except they’re meant to represent something that is created anew every time it is injected into a controller.

Controllers

Controllers are blocks of code meant to be tied directly to blocks of the DOM (either directly, via routes, or via directives).

Directives

Directives are constructs in AngularJS that allow you to define new HTML elements or attributes that can be applied at runtime to existing elements.

Scope

Probably the hardest part of AngularJS to grasp is the concept of ‘scope’. It’s not the same as JavaScript scope. In AngularJS, scope can be considered the same as the model in the MVC pattern; it’s bound to the view via the controller. AngularJS does dirty checking on the scope to incorporate two-way binding. That just means changes in the DOM to scope-bound properties will immediately be available in the controller, while changes in the controller that are done within an angular digest get updated in the DOM. Generally, there’s no need for manually binding event listeners.

If you want to learn more about AngularJS, the tutorial on angularjs.org is an excellent place to start.

Technology: Chrome Extension APIs

Chrome JavaScript APIs for extensions are really a pain in the ass. In order to use functionality, extensions must ask for permissions. However, Google doesn’t offer read-only permissions. This caused a lot of contention in the permissions request for New Tab Redirect 3.0. If you’re writing an extension and you plan to add some functionality that requires a permission that says ‘Read and modify’, my suggestion is that you don’t add that feature.

In order to create an ‘Apps’ page that somewhat resembled the old New Tab page, I needed:

  • bookmarks (permission: bookmarks, Read and modify your bookmarks)
  • Most Visited Sites (permission: topsites, Read and modify your browsing history.. yeah the description is stupid)
  • Apps management (permission: management, Manage your apps, extensions, and themes)
  • chrome://favicon/ (no permissions needed)

The permissions I needed were only:

  • pulling up to 40 bookmarks from the bookmarks bar only
  • querying top sites (the API call only gives you 20 sites, why describe it as Read and modify your browsing history?)
  • apps management, not extensions and not themes

I can only imaging that Google has a hard enough time as a company, considering how they’ve dropped quite a few products that I loved. They’ve also invested a lot of time in technologies like Google Glass and cars that can drive themselves. They’re likely not at all interested in making a more robust permissions system for extension developers.

The permissions required for the New Tab Redirect ‘Apps’ page are what they are. So many users got scared and even went as far as to falsely accuse me of anonymously collecting their data. The extension is open source, so accuse away: you’re completely wrong. Then again, people that use Google Chrome and think Google is not stealing gobs and gobs of their data are ignorant to the many mechanisms built into Chrome specifically to steal their data. Consider the data stored at chrome://predictors/: if Google has 1 billion Chrome users, they can now collect data from 1 billion distributed machines that tells them what users type into the address bar and where that user ends up navigating. Extension developers are the least of your worries (like you, I don’t trust extension developers either).

The code, a brief walkthrough

New Tab Redirect previously loaded redirect.js, which simply redirected to a page defined on the options page. I wanted to keep this functionality, but at the same time I did not want the New Tab Redirect ‘Apps’ page to start loading if the user defiend an optional redirect.

app.js

AngularJS makes that easy. You can provide a flag to defer bootstrapping of the application until you explicitly tell angular ‘Go!’:

window.name = 'NG_DEFER_BOOTSTRAP!';

This just has to be declared before you call angular.run(). Then, when you’re ready, you call:

angular.resumeBootstrap();

Then, angular will begin wiring itself up to the page. In this way, the new ‘Apps’ page DOES NOT LOAD unless the user is using that page.

One interesting hurdle when writing an AngularJS application for Google Chrome Extensions is that AngularJS wraps some standard HTML elements like a, input, img, and form. I’m only using anchor tags and images in New Tab Redirect, but I didn’t undrestand why images for Apps wouldn’t load. The problem is that for images, you have to whitelist the chrome protocol, and for anchor hrefs you need to whitelist chrome-extension so the AngularJS compiler will be happy with the rendered HTML. This all gives the following clean app.js:

'use strict';
// Setting the window.name property in this way allows us to call app.run(), but block until it's ready to be resumed.
// the resume happens in redirect.js if no redirected new tab url has been specified.
window.name = 'NG_DEFER_BOOTSTRAP!';
var app = angular.module('newTab', ['newTab.controllers', 'newTab.directives', 'newTab.filters']);

app.config(['$compileProvider', function($compileProvider) {
    // see https://github.com/angular/angular.js/issues/3889
    $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob|chrome):|data:image\//);
    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|chrome|chrome-extension):/);
}]);

app.run();

controllers.js

The new ‘Apps’ page has a single controller on the main page. When it loads, it checks for the user’s synced preferences, then loads apps followed by bookmarks and top sites only if the user wants to load them. The cool thing about AngularJS is how clean doing this becomes (I’ve removed function logic below for brevity full file here):

'use strict';
var controllers = angular.module('newTab.controllers', ['newTab.services']);

controllers.controller('MainController', ['$scope', 'Apps', function ($scope, Apps){
    var enable_top_key = 'ntr.enable_top',
        enable_bookmarks_key = 'ntr.enable_bookmarks',
        bookmarks_count_key = 'ntr.bookmark_count',
        top_count_key = 'ntr.top_count';

    $scope.extension_name = "New Tab Redirect!";
    $scope.enable_bookmarks = false;
    $scope.enable_top = false;
    $scope.bookmarks = [];
    $scope.show_prefs = false;
    $scope.bookmark_count = 10;
    $scope.top_count = 10;

    $scope.save_preferences = function(){ };

    function loadBookmarks() { }
    function loadTopSites() { }
    function loadApps() { }

    $scope.$on('UninstalledApp', loadApps);

    // initial page setup
    var querySettings = [enable_top_key, enable_bookmarks_key, bookmarks_count_key, top_count_key];
    Apps.getSetting(querySettings)
        .then(function(settings){
            // assign settings to scope
        })
        .then(function(){
            loadApps()
                .then(function(){
                    loadBookmarks();
                    loadTopSites();
                });
        })
        .then(function setupWatches(){
            $scope.$watch('bookmark_count', loadBookmarks);
            $scope.$watch('top_count', loadTopSites);
        });
}]);

When you declare a Controller, you can pass just a function as the second parameter and the parameter names will tell AngularJS how to look up services, factories, or other injectables so they’re available in your controller. Another way to do this is as I’ve done it and pass an array where the beginning of the array are the names of the dependencies and the last element of the array is the function of the Controller. This style allows you to later minify code without breaking AngularJS’s dependency injector.

You’ll notice here that Apps.getSetting is a Promise. Promises make code way cleaner. The way the getSetting promise is setup here allows me to set all settings to their properties on the scope, then load apps (always done), then conditionally load bookmarks and top sites. After everything is loaded, a $scope.$watch call allows us to say that anytime bookmark_count or top_count changes, call the relevant function again. If I had bound these watch functions anytime before in this promise chain, these functions would be called multiple times. Each function simply delegates to the Apps service call and returns a function, so that code is omitted from the above logic.

services.js

The services file actually contains a single service, Apps. The service has the following interface:

{
    getAll: function () { },

    launch: function(id){ },

    pinned: function(url){ },

    newWindow: function(url){ },

    uninstall: function(id){ },

    tab: function(url){ },

    navigate: function(url){ },

    topSites: function(){ },

    saveSetting: function(obj){ },

    getSetting: function(obj) { },

    getBookmarksBar: function(limit){}
};

A better design interface would have probably been to create a service facade around the ‘Tabs functionality (pinned, newWindow, tab) and the Config functionality (saveSetting, getSetting). To keep it simple I used only ‘Apps’.

Each of these functions returns a promise to allow for clean chaining of asynchronous functions.

Rather than explain each function in depth, I’ll cover just the first one and you can look at the others on GitHub.

Apps.getAll() will retrieve all apps using the chrome.management.getAll API call provided by chrome:

getAll: function () {
    var deferred = $q.defer();

    chrome.management.getAll(function (results) {
        $rootScope.$apply(function(){
            deferred.resolve(results);
        });
    });

    return deferred.promise;
},

The problem here is that AngularJS is only aware of changes on the scope that occur during the digest loop by explicitly applying ‘regular JavaScript’ to the AngularJS internals. This is done with $rootScope.$apply whenever you have some data that you want AngularJS to consider. Chrome doesn’t provide error callbacks on the API because errors are handled within the browser, making it easy for client applications like this to handle logic. A ‘deferred’, created by $q.defer(); is how JavaScript code can promise a future value to callers and is not AngularJS-specific; jquery has deferreds, Kris Kowal has an excellent Promises implementation called q, and versions of the CommonJS Promises proposal is implemented in many other frameworks or utilities.

A deferred object has two states: success and failure. To trigger the success state of a deferred object (which completes the promise successfully), you would call deferred.resolve. To initiate an error, you’d call deferred.reject. You can chain promises by returning a new promise from a chainable function, usually .then(fn).

HTML

I’m going to cheat a little here and show you the HTML before the directives. I’ll only show two snippets of HTML: one contained in main.html and a template.

In main.html you’ll see:

<div class="container app-container clear" ng-class="{'after-bookmarks': enable_bookmarks && bookmarks.length > 0,'populated':apps.length > 0}">
    <div><input type="search" ng-model="q.name" ng-show="apps.length > 5" placeholder="Filter apps"></div>
    <chrome-app ng-repeat="app in (apps | filter:q)" app="app"></chrome-app>
    <div ng-show="(apps | filter:q).length == 0" style="margin:1.5em">No matches found.</div>
    <span class="clear"></span>
</div>

This actually has a lot of AngularJS stuff in it. The opening div tag has this weird ng-class attribute in which the content looks like a JSON object. This is one of many built-in AngularJS directives. It applies the class ‘after-bookmarks’ based on the condition in the value of the property at runtime (during a digest loop). This means that any time enable_bookmarks changes, the classList might change from “container app-container clear” to “container app-container clear after-bookmarks”. If the ‘populated’ condition later changes, we’d have “container app-container clear after-bookmarks populated”. Then, the user could disable the bookmarks setting and AngularJS would automatically update the classList to “container app-container clear populated”. There’s no additional work you need to do.

Next, there’s an input type="search" that has an ng-model and ng-show attribute. Usually, something applied to ng-model will represent a property on your $scope object. So, if you had ng-model="favorite.color", and in your controller, you’ve set $scope.favorite = { color: 'blue' }, the value of the text box would read ‘blue’. If you changed the input text from ‘blue’ to ‘red’, you would immediately have the true condition in your controller $scope.favorite.color === 'red'. The ng-show toggles the display:none style of the element based on the condition.

Next, there’s this weird non-standard XML element, <chrome-app ng-repeat="app in (apps | filter:q)" app="app"></chrome-app>. That’s a directive I’ve defined and will discuss later. The attribute, ng-repeat acts as a foreach loop. The value of that attribute says for each value in apps | filter:q, apply app to the ‘app’ attribute of my custom directive, and generate the templated structure. The apps | filter:q syntax is how AngularJS declaratively applies filters (another available application module like a service or factory). The | is what actually applies the filter, the filter:q is how we define what filter to call and what parameter to pass the filter. In standard JavaScript, this might look like:

var apps = [];
var filtered = apps.filter( function filter(q) {
    // filter:q logic here.
});

In AngularJS, the filter filter is way more involved.

The <chrome-app></chrome-app> directive will take the ‘app’ object assigned to the ‘app’ attribute (app="app") and apply it to the following template:

<div class="app-icon">
    <a href="app.appLaunchUrl" chrome-launch="app.id" chrome-type="app.type" class="app-icon-128">
        <img src="{{app.icons|iconsize:128:app}}" title="{{app.name}}"/>
        <span class="app-desc">{{app.name}}</span>
    </a>

    <div class="app-actions">
        <a href="app.appLaunchUrl" ng-if="app.type != 'packaged_app'" chrome-pinned="app.id"
           title="Open {{app.name}} in a pinned tab"><i class="fa fa-2x fa-thumb-tack"></i></a>
        <a href="app.appLaunchUrl" ng-if="app.type != 'packaged_app'" chrome-new-tab="app.id"
           title="Open {{app.name}} in a new tab"><i class="fa fa-2x fa-level-up"></i></a>
        <a href="app.appLaunchUrl" ng-if="app.type != 'packaged_app'" chrome-new-window="app.id"
           title="Open {{app.name}} in a new window"><i class="fa fa-2x fa-external-link"></i></a>
        <a href="app.optionsUrl" chrome-options="app.id" ng-if="app.optionsUrl" title="Open options for {{app.name}}"><i
                class="fa fa-2x fa-wrench"></i></a>
        <a href="#" chrome-uninstall="app.id" title="Uninstall {{app.name}}"><i class="fa fa-2x fa-trash-o"></i></a>
    </div>
</div>

The app object is the result object from chrome’s API call.

The new thing in this snippet is the introduction of {{ somePropertyName }}. This is an interpolation in AngularJS. It’s actually not that performant a lot of the time, but for something like title="{{app.name}}" it is usually the only way to dynamically set string contents. Here you see another AngularJS directive, ng-if, which conditionally adds to or removes from the DOM the whole element it’s applied to. Then, you see other custom directives, chrome-pinned, chrome-new-tab, chrome-new-window.

With these examples, you now can understand probably 75% of the code you’d find in AngularJS

directives.js

The last bit to cover is directives. This is my favorite aspect of AngularJS so I’ll try really hard not to ramble on the topic.

AngularJS lets you define directives to run for any attributes, elements, or CSS class names. I don’t generally use the class feature. Directives allow you to hook into the compile phase or the link phase (but not both simultaneously). You can also define a controller or explicitly create a child scope or isolated scope for the given element(s). I like to keep things simple and stick to adding functionality in the link phase.

Here’s an example:

directives.directive('chromePinned', ['$log', 'Apps', function($log, Apps){
    return {
        // attribute only
        restrict: 'A',

        scope: {
            id: '=chromePinned',
            url: '=href'
        },

        link: function($scope, $element, $attrs) {
            if($scope.id){
                $element.bind('click', function(e){
                    e.preventDefault();
                    Apps.pinned($scope.url)
                        .then(function(tab){
                            $log.debug("Opened app id %s in pinned tab #%d", $scope.id, tab.id);
                        });
                });
            }
        }
    };
}]);

The directive here is declared in the same dependency injection style as the controller, where we define the dependency names and function in an array to prevent breaking the injector if the code is ever minified. The directive must return and object that represents a directive definition.

This directive is restricted to only work on attributes (however, restrict:'AEC' would work on attributes, elements, and class names). Defining an object for the scope property creates an isolated scope where id is data-bound to the property of chromePinned (in attribute format it is chrome-pinned‘s value) and url is bound to href. Isolated scopes break the directive out of the scope hierarchy that AngularJS maintains by default. Isolated scopes can take some getting used to, but I think they’re safer and easier to follow.

The linking function binds a click event to the element (e.g. an anchor tag of <a href="#" chrome-pinned="12345">). Any on click, we call Apps.pinned with the url defined by the anchor’s href attribute. Whenever you work on an element in a linking function, you’re working in standard JavaScript (i.e. outside of AngularJS’s digest). You should always call $scope.$apply with whatever function you want to make AngularJS aware of. In this directive, you don’t see that call to apply because it’s done within the service itself.

Conclusion

In all, the new feature was quick and painless to code. The files are clean, easy to read, and well organized.

The code, as always is on github: jimschubert/NewTab-Redirect

Flattr this!

log4net configuration in .NET 3.5

Step 1 Add Reference
Right click on the “References” folder and choose add reference. Browse to the location of log4net and add it to the project.

Step 2 Add Config Section Reference
In web.config, add a reference to the log4net config section handler. This will look like:

<?xml version="1.0"?>
<configuration>
 <configSections>
  <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
 <!-- other sections/sectionGroups -->
 </configSections>
</configuration>

Step 3 Add the log4net configuration Section
All that is left is to add the actual configuration. There are an unlimited number of ways this can be done, with numerous different logs and log types. An example configuration follows. This configuration creates a log for NHibernate with a max size of 8MB, a rolling file (max size 3MB), and a a console log. Anything logged to the logger named NHibernate.SQL will record only if they are ERROR level or higher. Anything logged without specifying a logger name is logged to root, and only goes to the console and rolling file.

 <log4net>
  <appender name="NHibernateFileLog" type="log4net.Appender.RollingFileAppender,log4net">
   <file value="logs/nhibernate.txt"/>
   <appendToFile value="true"/>
   <rollingStyle value="Size"/>
   <maxSizeRollBackups value="0"/>
   <maximumFileSize value="8MB"/>
   <staticLogFileName value="true"/>
   <layout type="log4net.Layout.PatternLayout,log4net">
    <conversionPattern value="%d [%t] %-5p %c - %m%n"/>
   </layout>
  </appender>
  <appender name="console" type="log4net.Appender.ConsoleAppender, log4net">
   <layout type="log4net.Layout.PatternLayout,log4net">
    <param name="ConversionPattern" value="%d{ABSOLUTE} %-5p %c{1}:%L - %m%n"/>
   </layout>
  </appender>
    <!-- log4net uses 5 levels, namely DEBUG, INFO, WARN, ERROR and FATAL. -->
  <appender name="rollingFile" type="log4net.Appender.RollingFileAppender,log4net">
   <param name="File" value="logs/RollingLog.txt"/>
   <param name="AppendToFile" value="false"/>
   <param name="RollingStyle" value="Date"/>
   <param name="DatePattern" value="yyyy.MM.dd"/>
   <param name="StaticLogFileName" value="true"/>
   <maximumFileSize value="3MB"/>
   <maxSizeRollBackups value="0"/>
   <staticLogFileName value="true"/>
   <layout type="log4net.Layout.PatternLayout,log4net">
    <param name="ConversionPattern" value="%d{HH:mm:ss.fff} [%t] %-5p %c - %m%n"/>
   </layout>
  </appender>
  <logger name="NHibernate.SQL" additivity="false">
   <level value="ERROR"/>
   <appender-ref ref="NHibernateFileLog"/>
   <appender-ref ref="console"/>
   <appender-ref ref="rollingFile"/>
  </logger>
  <root>
   <level value="ERROR"/>
   <appender-ref ref="console"/>
      <appender-ref ref="rollingFile"/>
   <!--Uncomment the following appender for verbose output (degrades performance)-->
   <!--<appender-ref ref="rollingFile"/>-->
  </root>
 </log4net>
 

Step 4
Now, all that’s left is to call log4net’s configure method before any other (read: **error prone**) code is called.
For example, in an ASP.NET Web application, you could add this to Global.asax:

public class GlobalAsax : HttpApplication
{
    void Application_Start(object sender, EventArgs e)
    {
        log4net.Config.XmlConfigurator.Configure();
    }
}

Other logging options include database, mail, net, access.

For more information and examples on log4net, visit http://logging.apache.org/log4net/release/config-examples.html

An example Logger implementation in C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using log4net;
using log4net.Config;
namespace ETeacherWeb.Common
{
    /// <summary>
    /// Logger class for log4net programmatic logging
    /// </summary>
    public static class Logger
    {
        /// <summary>
        /// Severity/Level of the log entry
        /// </summary>
        public enum LogLevel
        {
            DEBUG = 1,
            ERROR,
            FATAL,
            INFO,
            WARN
        }
        #region Members
        private static readonly ILog logger = LogManager.GetLogger(typeof(Logger));
        #endregion
        #region Constructors
        static Logger()
        {
            XmlConfigurator.Configure();
        }
        #endregion
        #region Methods
        /// <summary>
        /// Write a string to the log with a specified level of severity
        /// </summary>
        /// <param name="logLevel">The severity of the log entry</param>
        /// <param name="log">The log entry</param>
        public static void WriteLog(LogLevel logLevel, String log)
        {
            if (logLevel.Equals(LogLevel.DEBUG))
            {
                logger.Debug(log);
            }
            else if (logLevel.Equals(LogLevel.ERROR))
            {
                logger.Error(log);
            }
            else if (logLevel.Equals(LogLevel.FATAL))
            {
                logger.Fatal(log);
            }
            else if (logLevel.Equals(LogLevel.INFO))
            {
                logger.Info(log);
            }
            else if (logLevel.Equals(LogLevel.WARN))
            {
                logger.Warn(log);
            }
        }
        #endregion
    }
}
 

To use the above code, you would call

 Logger.WriteLog(LogLevel.DEBUG, "The expected logic failed validation");

Flattr this!

Google Chrome Extension: New Tab Redirect! STEP BY STEP

If you’ve reached this this page in an attempt to find instructions for uninstalling the extension here they are:

  1. Navigate to chrome://extensions
  2. Click ‘Uninstall’ below ‘New Tab Redirect!’

Yes, it’s that easy.

If you’re doing this because you can no longer find your apps, add a bookmark called “Apps” that points to

chrome-internal://newtab

Finally, if you’re wondering what magic was used to create this extension, read on.

Background

(see http://code.google.com/chrome/extensions/overview.html for a thorough background of Chrome Extension development)

A Google Chrome Extension is a mini web application which runs in its own process and can perform actions at a browser-level or page-level.

An extension consists (or may consist) of a number of parts:

  • manifest.json
  • Web files (html, js, css, images)
  • NPAPI Plugins
  • background.html (the long-running process)

All of these files are bundled into a package with a .crx file extension. When uploading your extension to the Google Chrome Extensions Gallery, you only zip up the files your extension needs and Google will package it properly for you.
The manifest.json file is a file which specifies properties of your extension in JavaScript Object Notation (aka JSON). For a quick intro to JSON, check out http://www.learn-ajax-tutorial.com/Json.cfm

The Manifest File

manifest.json:

 {
   "name": "New Tab Redirect!",
   "description": "Sets a user-specified URL to load in new tabs.",
   "version": "0.0.1.109",
   "background_page": "background.html",
   "options_page":"options.html",
   "chrome_url_overrides": {
     "newtab": "redirect.html"
   },
      "permissions": ["tabs"],
      "icons": {
      "128": "icon128.png",
      "19":"icon19.png"
    }
  }

(end manifest.json)

The name, description, version, background_page, and options_page key/value pairs are pretty self-explanatory.
The “chrome_url_overrides” section is interesting. As you can see, the value for this entry is itself an object. This object allows me to override “newtab” with my own page, “redirect.html”. Currently, the chromium team will only allow you to override newtab, however, I anticipate they’ll allow you to override other pages in the future. For a list of url constants used within Chrome, check out the url_constants.cc file in the source code repository. Possible future overrides would be anything that is listed near the end of that file as ‘const char kChromeUI*Host[]’

Back to the file. “permissions” takes an array of requested permissions. New Tab Redirect! requires permissions to update tabs. This is because we’re not only redirecting to html web-hosted sites; New Tab Redirect allows you to set web pages, local files, or inherent Chrome pages.

“icons” specifies the location of the icons your extension will use. These icons will be displayed in the Gallery and in the Extensions page within Google Chrome.

Remember: You must change your version number when updating your publicly hosted extension, or else Chrome won’t find a new version and your users will not be updated.

The Background Page

background.html


 <html>
 <head>
 <script type="text/javascript">
 String.prototype.startsWith = function(str){
     return (this.indexOf(str) === 0);
 }
  
 var url = null;
 var newTabId = undefined;
  var protocol =  undefined;
  var allowedProtocols = ["http://","https://","about:","file://", 
    "file:\\", "file:///", "chrome://","chrome-internal://"];
   
  function setUrl(url) {
          if(protocolPasses(url) 
            && url.length >  { /* 8 is arbitrary */
              this.url = url;     
          } else {
              protocol = 'http://'; /* force http */
              var right = url.split('://')
              if(right != undefined && right != null && right.length > 1) {
                  this.url = protocol + right[1];
              } else { 
                  /* this will redirect to http:// if url is empty */
                  this.url = protocol + url; }    
          }
          localStorage["url"]  = this.url;
          
      function protocolPasses(url) {
          if(typeof(url) == 'undefined' || url == null) { return false; }
          if (url.startsWith(allowedProtocols[3]) 
              && !url.startsWith(allowedProtocols[5])) {
              url.replace(allowedProtocols[3], allowedProtocols[5]);
          } else if (url.startsWith(allowedProtocols[4])) {
              url.replace(allowedProtocols[4], allowedProtocols[5]);
          }
          
          for(var p in allowedProtocols) {
              if(url.startsWith(allowedProtocols[p])) { return true;}
          }
          return false;
      }
  }
   
  function init() {
     url = localStorage["url"] || "http://www.facebook.com";
  }
   
  function r(tabId) {
      chrome.tabs.update(tabId, {"url":this.url});
  }
   
  chrome.tabs.onUpdated.addListener(function(tabId,info,tab) {
      if (info.status === 'loading')
          console.log('Loading url ... ' + tab.url)
      if(info.status === 'complete')
          console.log('Finished loading url ... ' + tab.url)
  });
   
  chrome.tabs.onCreated.addListener(function(tab) {
      newTabId = tab.id
  });
  </script>
  </head>
  <body onload="init()"></body>
  </html>

(end background.html)

This page is pretty self-explanatory for the most part. The background page is your long-running process, and is usually used to house any variables that are shared between pages. However, this isn’t a necessity of Chrome Extension architecture. Every page in an extension can interact with any other page through chrome.extension.getViews() or chrome.extension.getBackgroundPage() . Because of the nature of New Tab Redirect, I’ve used a background page.

One thing of note is the initialization of the extension. Since the operation is fairly simple, allowing a user to specify a url and then redirecting to that url, the only thing I need to worry about immediately is the url. This is stored using HTML 5’s local storage. If the url doesn’t exit, I set it to facebook.

The setUrl function is located in the Background page and is called from options.html. It is located here so the options page isn’t trying to set the url variable that is maintained by the background page. This function checks my array of allowed protocols and validates the url with some simple rules.

r(tabId) is the function which actually updates the tab when the protocol isn’t an http or https protocol. The function name is short because the the code in redirect.html should be as concise as possible. This function calls the chrome.tabs.update method (one culprit behind the tabs permission requirement)

That tabId is provided via the redirect.html, which gets the currently created tab’s id. However, again note that this processing only occurs when the url is local.

Options

Because the options page is simple html and styles, I’ll only include the JavaScript which sets the url.

options.js

String.prototype.startsWith = function(str) {
     return (this.indexOf(str)===0);
 }
 var chromePages = {
     Extensions : "chrome://extensions/",
     History : "chrome://history/",
     Downloads : "chrome://downloads/",
     NewTab : "chrome-internal://newtab/"
 }
  var aboutPages = ["about:blank","about:version", "about:plugins","about:cache", 
      "about:memory","about:histograms","about:dns",
      "chrome://extensions/","chrome://history/",
      "chrome://downloads/","chrome-internal://newtab/"];
  var popularPages = { 
      "Facebook":"www.facebook.com",
      "MySpace":"www.myspace.com",
      "Twitter":"www.twitter.com",
      "Digg":"www.digg.com",
      "Delicious":"www.delicious.com",
      "Slashdot":"www.slashdot.org"
  };
   
      
  // save options to localStorage.
  function save_options() {
      var url = $('#custom-url').val();
      if(url == ""){
          url = aboutPages[0];
      }
      
      if( $.inArray(String(url), aboutPages) || isValidURL(url)) {
          save(true,url);
      } else {
          save(false,url);
      }        
  }
   
  function save(good,url) {
      if(good) {
          chrome.extension.getBackgroundPage().setUrl(url);
          $('#status').text("Options Saved.");
      } else {
          $('#status').text( url + " is invalid. Try again (http:// is required)");    
      }
      
      $('#status').css("display", "block");
      setTimeout(function(){
          $('#status').slideUp("fast").css("display", "none");
      }, 1050);
  }
   
  // Restores select box state to saved value from localStorage.
  function restore_options() {
      var url = chrome.extension.getBackgroundPage().url || "http://www.facebook.com/";    
       $('#custom-url').val(url);
  }
   
  function isValidURL(url) {
      var urlRegxp = /^(http:\/\/www.|https:\/\/www.|ftp:\/\/www.|www.){1}([\w]+)(.[\w]+){1,2}$/;
      if (urlRegxp.test(url) != true) {
          return false;
      } else {
          return true;
      }
  }
   
  function saveQuickLink(url){
      var uurl = unescape(url);
       $('#custom-url').val(uurl);
       save(true,uurl);
       return false;
  }
   
  $(document).ready(function(){
      restore_options();
      $.each(chromePages, function(k,v) {
          var anchor = "<a href=\"javascript:saveQuickLink('"+v+"');\">"+k+"</a>";
          $('#chromes').append("<li>" + anchor + "</li>");
      });
      $.each(aboutPages, function() {
          if(this.startsWith("about:")) { /* quick fix to handle chrome pages elsewhere */
              var anchor = "<a href=\"javascript:saveQuickLink('"+this+"');\">"+this+"</a>";
              $('#abouts').append("<li>" + anchor + "</li>");
          }
      });
      $.each(popularPages, function(k,v) {
          var anchor = "<a href=\"javascript:saveQuickLink('"+v+"');\">"+k+"</a>";
          $('#popular').append("<li>" + anchor + "</li>");
      });
  });

(end options.js)

As you can see, I’ve included jQuery for dom manipulation. I decided to do this because jQuery is so small and because the Options page can be whatever size you’d like (keeping in mind user experience, of course). This saved me some time from writing out a little bit of javascript.

Of note here is how I build the lists of Chrome pages, About pages, and Popular pages. This isn’t included in the options.html file, which only has placeholders for the unordered lists. This allows me to go in and provide a page name and url for chromePages and popularPages, or just the url for aboutPages. If I want to add anything in the future, I’ll just add a key/value pair (or url) to one of these objects. If I want to remove, likewise, I only touch the respective object.

Quick links (one from an above-mentioned object) are saved automatically, bypassing the url checking regex. However, you’ll notice in the save_options function that I check first to see if the url is in the list of about pages, this is because a user will most likely enter about:memory or any valid url, and whould be less likely to enter chrome-internal://newtab/. In the future, I will probably add checks on all objects before checking a valid url, but this kind of thing happens in incremental development.

As you can see, if the user gets to save the url, the function setUrl from the background page is called via chrome.extension.getBackgroundPage().setUrl(url); Following good user interface techniques, we have to let the user know what happened, which is what the next line and the rest of the function accomplishes.

Note: The new tab url is chrome-internal:// instead of chrome://. Why is this, do you think? Don’t think too hard, though. It’s simple: redirect.html is now chrome://newtab/, because I’ve told chrome to override the newtab with my own file. You can test it out by typing chrome://newtab into Google Chrome. Try chrome-internal://newtab and see what happens? This can only be called internally, hence the name, and must be called from within an extension via chrome.tabs.update. If you don’t use this internal url, when you set the url to chrome://newtab and hit CTRL+T or click the ‘+’ tab, your tab will keep trying to call itself, and eventually stop, after doing nothing meaningful.

Redirect.html

Finally, the beast that does it all.

redirect.html


  <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
  <html>
    <head>
      <meta name="generator" content=
      "HTML Tidy for Windows (vers 14 February 2006), see www.w3.org">
      <title>
        Redirecting...
      </title>
      <script type="text/javascript">
       var wid, tid;
    
       function r() {
           var url = localStorage["url"] || "";
           if (url.toLowerCase().indexOf("http:") === 0 || 
               url.toLowerCase().indexOf("https:") === 0) {
               document.location.href = url;
               return;
           } else {
               chrome.windows.getCurrent(function(w) {
                   wid = w.id;
                   chrome.tabs.getSelected(wid, function(t) {
                       tid = t.id;
                   });
               });
               chrome.extension.getBackgroundPage().r(tid);
           }
       }
   </script>
     </head>
     <body onload="setTimeout('r()',100);r();return false;">
       Redirecting...<br>
       <em>If your page doesn't load within 5 seconds, 
         <a href="javascript:r()">click here</a></em>
     </body>
   </html>

(end redirect.html)

Note: I have run the redirect.html file through HTML Tidy and a JavaScript formatter at http://jsbeautifier.org/, but this file should be as compact as possible. If you were to download New Tab Redirect and look at the source code, you’d see this code is all on one line. I’m writing this for your enjoyment, though, and one-liners aren’t fun to look at.

The only thing special about this file is the the onload function. You’ll notice how it runs r after 100 ms, then runs it again. There is quite an odd situation here: Chrome either has to initialize localStorage, doesn’t allow us to get the current window and tab id immediately, or the processing required to do so takes longer than 100ms. You could, honestly, run the redirect function through a loop until it redirects (considering the document.location.href will immediately redirect the browser tab. If the url specified by the user is local, the function gets the current window, and from that the id of the current tab, and passes that to the background page’s function.

I’ve been asked by a user or two to remove the redirect message, but I’ll keep it for now. I consider a message necessary if, for some reason, a future release of Chrome breaks the extension.

The current source code for New Tab Redirect can be accessed via your Chrome Extensions directory after installing the extension, and is availing at the New Tab Redirect Project Page on Google Code.

If you have any questions or comments about this extension, please contact me.

Flattr this!

My Development Blog

When I began my internship with Agility Healthcare Solutions (now part of GE Healthcare), I thought it would be beneficial to keep a blog of my activities. Originally, I intended to keep a blog of problems I found or areas I found difficult, in case I ever encountered these problems again. Then, I realized my blog could be beneficial to others if I post random code. Earlier stages of my blog focus on difficulties I have had, while later stages are just examples of my code.

Please, feel free to follow my blog and make comments!

The Link: Blogger: James Schubert

Flattr this!

Quake Tracker

The Project

I love Google products! Again, I am a huge fan of Google products, so I decided to explore the APIs and tools which Google offers.

Google Maps API is a framework which allows you to use Google’s Maps in your own site, overlaying your own content onto the map. I wanted to create a map for a long time, but I didn’t have any ideas for what I could overlay.

The Solution

When I was younger, I wanted to be a seismologist. I think this originated from the Pierce Brosnan movie, “Dante’s Peak”. Recently, my wife and I watched this film and it was not as factual as I once believed. Perhaps the best part of the movie, was when Brosnan, a volcanologist, is in the truck with the town’s mayor. She is driving and approaches a river of lava. She asks, “Do you think it’s safe to drive over it?” Brosnan says, “I don’t know!” Wow!

With that background, I think it’s understandable that I wanted to track earthquakes! Although this project isn’t finished, I think it is fun to open it and see where the earth is quaking. This page uses jQuery and pulls an XML feed for earthquakes between M2 and M5 from usgs.gov, which is updated regularly. That means what you see is always up-to-date!

The Link: Quake Tracker

Flattr this!

Calculator (Java/GWT)

The Project

My continuing goal in application development is to learn new technologies and explore my possibilities. I am a huge fan of Google products, so I decided to explore the APIs and tools which Google offers.

Google Web Toolkit is a framework which allows you to write web content in Java, and convert the code to JavaScript and HTML designed to work the same across browsers. I really enjoy using frameworks which make development faster and easier, such as Google Web Toolkit and jQuery.

This was my first attempt at creating anything with Google Web Toolkit. After following the example tutorials on Google’s site, this was very easy.

The Link: Open in New Window/Tab or Open as pop-up

Here’s a link to the code on github.

Flattr this!

Meta Tag Generator

The Project

My wife and her friend are starting a copy writing business. I wrote this tool to help with SEO. Although simple, the application was written to make html code generation point-and-click.

This is a Web Start Application written in Java. It’s fairly simple, with a Swing interface. It’s not meant to include all meta tag possibilities. If you have any questions please feel free to contact me.

The Link: Meta Tag Generator

Flattr this!

INFO 465 – Projects in IS

The class:

“The student’s behavioral and technical skills developed in listed prerequisite courses are challenged by participating in a team systems development project. Appropriate computer-assisted software engineering (CASE) tools are used throughout the project, from requirement specification to implementation and testing.” (VCU Course Description)

The assignment:

The semester project was to create a site for a two-man landscaping company. The assignment is designed to allow students to better understand EDI concepts, requirements gathering, inventory and customer management, double-entry accounting, and other real-world topics.

Many students decided to use WinForms on Access databases. To switch things up, I used ASP.NET with C# code on a SQL Server 2005 database. The site may look very simple, the reason for this is a requirement to have our applications be presentable on small screens such as netbooks or PDAs. While my site is not explicitly designed for mobile devices, it does look good on small screens such as iPod Touch.

The site which is currently hosted is not the complete site. In fact, it is the product about 2/3 the way through the semester. At this point in the semester, we were given another project. I developed both sites on my local machine, and never uploaded the final product. After graduation, I have been unable to upload the final project.

You may log into the system using the username “ralph” and the password “cheese”.

The Link: INFO 465 Example (site currently down)

Flattr this!

INFO 451 – ASP.NET

The class:

INFO 451 – ASP.NET sought to teach advanced e-commerce practices using ASP.NET and C#.

The assignment:

This was a semester-long group assignment, aimed at honing the skills of each individual, while teaching the real-world connection between all aspects of Information Systems: people, technology, and organization.

The solution:

My group and I decided to offer an online bookseller as our project. Because most teams had two group members, and ours had three, we decided to go the extra step and offer retail books and user-submitted stories.

There was a huge difference in levels of programming knowledge in our group, which slowed things down. However, I am confident to say that our finished product surpassed that of the other groups.

What stands out?

Our site uses an MVC/Factory pattern (not ASP.NET MVC Framework, however). The data access methods are abstracted for modularity, meaning a switch from SQL Server 2005 to Oracle or Access would only require rewriting the fundamental database operations class, instead of each object’s data access layer. Unfortunately, time constraints lead to mixing some of this code into the Data Access Layer. This would have been changed after graduation, but the passwords to access the host server were immediately deactivated.

The Link:  INFO 451 Example

Flattr this!

INFO 300 – Hardware & Software

The class:

Principles of computer hardware and software architecture, organization and operation. Introduction to data structures.

The assignment:

You assignment is to write a short research paper with a minimum of three references on the topic of web accessibility. Additionally you are to construct website in your web space so that it launches from your link at info300.net and works appropriately with whatever ‘web page reader’ software you’ve loaded on your PC or notebook computer. Your home page should have a link to your short paper.

On or before the due date for your assignment place the topic, outline, and references for it in a directory just off your home directory named Outlines. Place the title & topic for the brief on the first line of a file named Brief1, then double-space and follow it with the outline and references, using indentation to highlight the structure of your document. Use chmod to set the permissions on the Outlines directory to 700 and the file Brief1 to 600. For the website part of your project, make a directory named ‘web’ just off your home directory. Set its permissions to 701. Your ‘home page’ must be in the web directory, named exactly ‘index.html’. The permissions should be set to 604 for each of the web pages. There should be at least one other page with the technical brief that links back to your home page.

The Link:   INFO 300 Example

Flattr this!