 core/includes/form.inc                             |   13 +
 core/misc/form.autosave.js                         |   74 ++++
 core/misc/garlic/garlic.js                         |  425 ++++++++++++++++++++
 .../node/lib/Drupal/node/NodeFormController.php    |    5 +-
 .../lib/Drupal/node/Tests/NodeCreationTest.php     |   10 +
 .../node/lib/Drupal/node/Tests/PageEditTest.php    |    9 +
 .../lib/Drupal/system/Tests/Form/AutoSaveTest.php  |   80 ++++
 core/modules/system/system.module                  |   28 ++
 .../tests/modules/form_test/form_test.module       |   51 +++
 9 files changed, 694 insertions(+), 1 deletion(-)

diff --git a/core/includes/form.inc b/core/includes/form.inc
index 4cdd101..ddd8a4f 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -969,6 +969,19 @@ function drupal_process_form($form_id, &$form, &$form_state) {
     }
   }
 
+  // Attach localStorage-powered auto form saving behavior.
+  if (!empty($form['#autosave'])) {
+    $form['#attached']['library'][] = array('system', 'drupal.formAutoSave');
+    $form['#attached']['js'][] = array(
+      'data' => array(
+        'formAutoSave' => array(
+          $form['#id'] => ctype_alnum($form['#autosave']) ? $form['#autosave'] : 1,
+        ),
+      ),
+      'type' => 'setting',
+    );
+  }
+
   // After processing the form, the form builder or a #process callback may
   // have set $form_state['cache'] to indicate that the form and form state
   // shall be cached. But the form may only be cached if the 'no_cache' property
diff --git a/core/misc/form.autosave.js b/core/misc/form.autosave.js
new file mode 100644
index 0000000..ef3fdf4
--- /dev/null
+++ b/core/misc/form.autosave.js
@@ -0,0 +1,74 @@
+(function ($, Drupal, drupalSettings) {
+
+"use strict";
+
+/**
+ * Automatically saves the contents of a form using localStorage when modified.
+ */
+Drupal.behaviors.formAutoSave = {
+  attach: function (context, settings) {
+    for (var formID in settings.formAutoSave) {
+      if (settings.formAutoSave.hasOwnProperty(formID)) {
+        var changed = settings.formAutoSave[formID];
+        $(context).find('#' + formID)
+          // Give the form a data- attribute to indicate its last modification.
+          .attr('data-localStorage-changed', changed)
+          // Attach garlic.
+          .garlic({
+            inputs: 'input[type!=file][type!=hidden], textarea, select',
+            expires: 86400 * 30,
+            conflictManager: {
+              enabled: false
+            },
+            getPath: generateGarlicLocalStorageKey
+          });
+      }
+    }
+  }
+};
+
+/**
+ * Override for garlic.js' localStorage key generation.
+ *
+ * garlic.js' getPath() implementation contains a lot of complexity, because
+ * they can't make any assumptions about the HTML of the form; because all forms
+ * in Drupal are generated by Form API, we can simplify a lot.
+ * We don't need to store the entire DOM node path, because each form item (as
+ * well as the form itself) is uniquely identified by its id attribute.
+ * We also don't need to look at the current URL thanks to
+ * drupalSettings.currentPath; without that, we could run into edge cases of
+ * e.g. www vs. no-www URLs of the same site.
+ * Finally, it takes a "last modification" identifier into account, which allows
+ * us to ignore localStorage data that doesn't apply anymore to the latest
+ * version of the object that the form allows the user to edit.
+ *
+ * @param jQuery $element
+ *   A jQuery element of a form item.
+ */
+function generateGarlicLocalStorageKey ($element) {
+  var key = '';
+  var $form = $element.closest('form');
+
+  // garlic.js prefix. Allows garlic.js to do clean-up.
+  key += 'garlic:';
+
+  // Drupal path at which this form lives.
+  key += drupalSettings.currentPath;
+
+  // "last modification" identifier.
+  key += '~';
+  key += $form.attr('data-localStorage-changed');
+
+  // Unique form identifier; CSS selector-like syntax.
+  key += '>';
+  key += 'form#' + $form.attr('id');
+  key += ' ';
+
+  // Unique form item identifier; CSS selector-like syntax.
+  key += $element.prop('tagName').toLowerCase();
+  key += '#' + $element.attr('id');
+
+  return key;
+}
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/misc/garlic/garlic.js b/core/misc/garlic/garlic.js
new file mode 100644
index 0000000..129fb44
--- /dev/null
+++ b/core/misc/garlic/garlic.js
@@ -0,0 +1,425 @@
+/*
+  Garlic.js allows you to automatically persist your forms' text field values locally,
+  until the form is submitted. This way, your users don't lose any precious data if they
+  accidentally close their tab or browser.
+
+  author: Guillaume Potier - @guillaumepotier
+*/
+
+!function ($) {
+
+  "use strict";
+  /*global localStorage */
+  /*global document */
+
+  /* STORAGE PUBLIC CLASS DEFINITION
+   * =============================== */
+  var Storage = function ( options ) {
+    this.defined = 'undefined' !== typeof localStorage;
+  }
+
+  Storage.prototype = {
+
+    constructor: Storage
+
+    , get: function ( key, placeholder ) {
+      return localStorage.getItem( key ) ? localStorage.getItem( key ) : 'undefined' !== typeof placeholder ? placeholder : null;
+    }
+
+    , has: function ( key ) {
+      return localStorage.getItem( key ) ? true : false;
+    }
+
+    , set: function ( key, value, fn ) {
+      if ( 'string' === typeof value ) {
+
+        // if value is null, remove storage if exists
+        if ( '' === value ) {
+          this.destroy( key );
+        } else {
+          localStorage.setItem( key , value );
+        }
+      }
+
+      return 'function' === typeof fn ? fn() : true;
+    }
+
+    , destroy: function ( key, fn ) {
+      localStorage.removeItem( key );
+      return 'function' === typeof fn ? fn() : true;
+    }
+
+    , clean: function ( fn ) {
+      for ( var i = localStorage.length - 1; i >= 0; i-- ) {
+        if ( 'undefined' === typeof Array.indexOf && -1 !== localStorage.key(i).indexOf( 'garlic:' ) ) {
+          localStorage.removeItem( localStorage.key(i) );
+        }
+      }
+
+      return 'function' === typeof fn ? fn() : true;
+    }
+
+    , clear: function ( fn ) {
+      localStorage.clear();
+      return 'function' === typeof fn ? fn() : true;
+    }
+  }
+
+ /* GARLIC PUBLIC CLASS DEFINITION
+  * =============================== */
+
+  var Garlic = function ( element, storage, options ) {
+    this.init( 'garlic', element, storage, options );
+  }
+
+  Garlic.prototype = {
+
+    constructor: Garlic
+
+    /* init data, bind jQuery on() actions */
+    , init: function ( type, element, storage, options ) {
+      this.type = type;
+      this.$element = $( element );
+      this.options = this.getOptions( options );
+      this.storage = storage;
+      this.path = this.options.getPath( this.$element ) || this.getPath();
+      this.parentForm = this.$element.closest( 'form' );
+      this.$element.addClass('garlic-auto-save');
+      this.expiresFlag = !this.options.expires ? false : ( this.$element.data( 'expires' ) ? this.path : this.getPath( this.parentForm ) ) + '_flag' ;
+
+      // bind garlic events
+      this.$element.on( this.options.events.join( '.' + this.type + ' ') , false, $.proxy( this.persist, this ) );
+
+      if ( this.options.destroy ) {
+        $( this.parentForm ).on( 'submit reset' , false, $.proxy( this.destroy, this ) );
+      }
+
+      // retrieve garlic persisted data
+      this.retrieve();
+    }
+
+    , getOptions: function ( options ) {
+      return $.extend( {}, $.fn[this.type].defaults, options, this.$element.data() );
+    }
+
+    /* temporary store data / state in localStorage */
+    , persist: function () {
+
+      // some binded events are redundant (change & paste for example), persist only once by field val
+      if ( this.val === this.$element.val() ) {
+        return;
+      }
+
+      this.val = this.$element.val();
+
+      // if auto-expires is enabled, set the expiration date for future auto-deletion
+      if ( this.options.expires ) {
+        this.storage.set( this.expiresFlag , ( new Date().getTime() + this.options.expires * 1000 ).toString() );
+      }
+
+      // for checkboxes, we need to implement an unchecked / checked behavior
+      if ( this.$element.is( 'input[type=checkbox]' ) ) {
+        return this.storage.set( this.path , this.$element.attr( 'checked' ) ? 'checked' : 'unchecked' );
+      }
+
+      this.storage.set( this.path , this.$element.val() );
+    }
+
+    /* retrieve localStorage data / state and update elem accordingly */
+    , retrieve: function () {
+      if ( this.storage.has( this.path ) ) {
+
+        // if data expired, destroy it!
+        if ( this.options.expires ) {
+          var date = new Date().getTime();
+          if ( this.storage.get( this.expiresFlag ) < date.toString() ) {
+            this.storage.destroy( this.path );
+            return;
+          } else {
+            this.$element.attr( 'expires-in',  Math.floor( ( parseInt( this.storage.get( this.expiresFlag ) ) - date ) / 1000 ) );
+          }
+        }
+
+        var storedValue = this.storage.get( this.path );
+
+        // if conflictManager enabled, manage fields with already provided data, different from the one stored
+        if ( this.options.conflictManager.enabled && this.detectConflict() ) {
+          return this.conflictManager();
+        }
+
+        // input[type=checkbox] and input[type=radio] have a special checked / unchecked behavior
+        if ( this.$element.is( 'input[type=radio], input[type=checkbox]' ) ) {
+
+          // for checkboxes and radios
+          if ( 'checked' === storedValue || this.$element.val() === storedValue ) {
+            return this.$element.attr( 'checked', true );
+
+          // only needed for checkboxes
+          } else if ( 'unchecked' === storedValue ) {
+            this.$element.attr( 'checked', false );
+          }
+
+          return;
+        }
+
+        // for input[type=text], select and textarea, just set val()
+        this.$element.val( storedValue );
+
+        // trigger custom user function when data is retrieved
+        this.options.onRetrieve( this.$element, storedValue );
+
+        return;
+      }
+    }
+
+    /* there is a conflict when initial data / state differs from persisted data / state */
+    , detectConflict: function() {
+      var self = this;
+
+      // radio buttons and checkboxes are yet not supported
+      if ( this.$element.is( 'input[type=checkbox], input[type=radio]' ) ) {
+        return false;
+      }
+
+      // there is a default not null value and we have a different one stored
+      if ( this.$element.val() && this.storage.get( this.path ) !== this.$element.val() ) {
+
+        // for select elements, we need to check if there is a default checked value
+        if ( this.$element.is( 'select' ) ) {
+          var selectConflictDetected = false;
+
+          // foreach each options except first one, always considered as selected, seeking for a default selected one
+          this.$element.find( 'option' ).each( function () {
+            if ( $( this ).index() !== 0 && $( this ).attr( 'selected' ) && $( this ).val() !== self.storage.get( this.path ) ) {
+              selectConflictDetected = true;
+              return;
+            }
+          });
+
+          return selectConflictDetected;
+        }
+
+        return true;
+      }
+
+      return false;
+    }
+
+    /* manage here the conflict, show default value depending on options.garlicPriority value */
+    , conflictManager: function () {
+
+      // user can define here a custom function that could stop Garlic default behavior, if returns false
+      if ( 'function' === typeof this.options.conflictManager.onConflictDetected
+        && !this.options.conflictManager.onConflictDetected( this.$element, this.storage.get( this.path ) ) ) {
+        return false;
+      }
+
+      if ( this.options.conflictManager.garlicPriority ) {
+        this.$element.data( 'swap-data', this.$element.val() );
+        this.$element.data( 'swap-state', 'garlic' );
+        this.$element.val( this.storage.get( this.path ) );
+      } else {
+        this.$element.data( 'swap-data', this.storage.get( this.path ) );
+        this.$element.data( 'swap-state', 'default' );
+      }
+
+      this.swapHandler();
+      this.$element.addClass( 'garlic-conflict-detected' );
+      this.$element.closest( 'input[type=submit]' ).attr( 'disabled', true );
+    }
+
+    /* manage swap user interface */
+    , swapHandler: function () {
+      var swapChoiceElem = $( this.options.conflictManager.template );
+      this.$element.after( swapChoiceElem.text( this.options.conflictManager.message ) );
+      swapChoiceElem.on( 'click', false, $.proxy( this.swap, this ) );
+    }
+
+    /* swap data / states for conflicted elements */
+    , swap: function () {
+      var val = this.$element.data( 'swap-data' );
+      this.$element.data( 'swap-state', 'garlic' === this.$element.data( 'swap-state' ) ? 'default' : 'garlic' );
+      this.$element.data( 'swap-data', this.$element.val());
+      $( this.$element ).val( val );
+    }
+
+    /* delete localStorage persistance only */
+    , destroy: function () {
+      this.storage.destroy( this.path );
+    }
+
+    /* remove data / reset state AND delete localStorage */
+    , remove: function () {
+      this.remove();
+
+      if ( this.$element.is( 'input[type=radio], input[type=checkbox]' ) ) {
+        $( this.$element ).attr( 'checked', false );
+        return;
+      }
+
+      this.$element.val( '' );
+    }
+
+    /* retuns an unique identifier for form elements, depending on their behaviors:
+       * radio buttons: domain > pathname > form.<attr.name>[:eq(x)] > input.<attr.name>
+          no eq(); must be all stored under the same field name inside the same form
+
+       * checkbokes: domain > pathname > form.<attr.name>[:eq(x)] > [fieldset, div, span..] > input.<attr.name>[:eq(y)]
+          cuz' they have the same name, must detect their exact position in the form. detect the exact hierarchy in DOM elements
+
+       * other inputs: domain > pathname > form.<attr.name>[:eq(x)] > input.<attr.name>[:eq(y)]
+          we just need the element name / eq() inside a given form
+    */
+    , getPath: function ( elem ) {
+      if ( 'undefined' === typeof elem ) {
+        elem = this.$element;
+      }
+
+      if ( this.options.getPath( elem ) ) {
+        return this.options.getPath( elem );
+      }
+
+      // Requires one element.
+      if ( elem.length != 1 ) {
+        return false;
+      }
+
+      var path = ''
+        , fullPath = elem.is( 'input[type=checkbox]' )
+        , node = elem;
+
+      while ( node.length ) {
+        var realNode = node[0]
+          , name = realNode.nodeName;
+
+        if ( !name ) {
+          break;
+        }
+
+        name = name.toLowerCase();
+
+        var parent = node.parent()
+          , siblings = parent.children( name );
+
+        // don't need to pollute path with select, fieldsets, divs and other noisy elements,
+        // exept for checkboxes that need exact path, cuz have same name and sometimes same eq()!
+        if ( !$( realNode ).is( 'form, input, select, textarea' ) && !fullPath ) {
+          node = parent;
+          continue;
+        }
+
+        // set input type as name + name attr if exists
+        name += $( realNode ).attr( 'name' ) ? '.' + $( realNode ).attr( 'name' ) : '';
+
+        // if has sibilings, get eq(), exept for radio buttons
+        if ( siblings.length > 1 && !$( realNode ).is( 'input[type=radio]' ) ) {
+          name += ':eq(' + siblings.index( realNode ) + ')';
+        }
+
+        path = name + ( path ? '>' + path : '' );
+
+        // break once we came up to form:eq(x), no need to go further
+        if ( 'form' == realNode.nodeName.toLowerCase() ) {
+          break;
+        }
+
+        node = parent;
+      }
+
+      return 'garlic:' + document.domain + ( this.options.domain ? '*' : window.location.pathname ) + '>' + path;
+    }
+
+    , getStorage: function () {
+      return this.storage;
+    }
+  }
+
+  /* GARLIC PLUGIN DEFINITION
+  * ========================= */
+
+  $.fn.garlic = function ( option, fn ) {
+    var options = $.extend(true, {}, $.fn.garlic.defaults, option, this.data() )
+      , storage = new Storage()
+      , returnValue = false;
+
+    // this plugin heavily rely on local Storage. If there is no localStorage or data-storage=false, no need to go further
+    if ( !storage.defined ) {
+      return false;
+    }
+
+    function bind ( self ) {
+      var $this = $( self )
+        , data = $this.data( 'garlic' )
+        , fieldOptions = $.extend( {}, options, $this.data() );
+
+      // don't bind an elem with data-storage=false
+      if ( 'undefined' !== typeof fieldOptions.storage && !fieldOptions.storage ) {
+        return;
+      }
+
+      // don't bind a password type field
+      if ( 'password' === $( self ).attr( 'type' ) ) {
+        return;
+      }
+
+      // if data never binded, bind it right now!
+      if ( !data ) {
+        $this.data( 'garlic', ( data = new Garlic( self, storage, fieldOptions ) ) );
+      }
+
+      // here is our garlic public function accessor, currently does not support args
+      if ( 'string' === typeof option && 'function' === typeof data[option] ) {
+        return data[option]();
+      }
+    }
+
+    // loop through every elemt we want to garlic
+    this.each(function () {
+
+      // if a form elem is given, bind all its input children
+      if ( $( this ).is( 'form' ) ) {
+        $( this ).find( options.inputs ).each( function () {
+          returnValue = bind( $( this ) );
+        });
+
+      // if it is a Garlic supported single element, bind it too
+      // add here a return instance, cuz' we could call public methods on single elems with data[option]() above
+      } else if ( $( this ).is( options.inputs ) ) {
+        returnValue = bind( $( this ) );
+      }
+    });
+
+    return 'function' === typeof fn ? fn() : returnValue;
+  }
+
+  /* GARLIC CONFIGS & OPTIONS
+  * ========================= */
+  $.fn.garlic.Constructor = Garlic;
+
+  $.fn.garlic.defaults = {
+      destroy: true                                                                               // Remove or not localstorage on submit & clear
+    , inputs: 'input, textarea, select'                                                           // Default supported inputs.
+    , events: [ 'DOMAttrModified', 'textInput', 'input', 'change', 'keypress', 'paste', 'focus' ] // Events list that trigger a localStorage
+    , domain: false                                                                               // Store et retrieve forms data accross all domain, not just on
+    , expires: false                                                                              // false for no expiration, otherwise (int) in seconds for auto-expiration
+    , conflictManager: {
+        enabled: true                                                                             // Manage default data and persisted data. If false, persisted data will always replace default ones
+      , garlicPriority: true                                                                      // If form have default data, garlic persisted data will be shown first
+      , template: '<span class="garlic-swap"></span>'                                             // Template used to swap between values if conflict detected
+      , message: 'This is your saved data. Click here to see default one'                         // Default message for swapping data / state
+      , onConflictDetected: function ( $item, storedVal ) { return true; }                        // This function will be triggered if a conflict is detected on an item. Return true if you want Garlic behavior, return false if you want to override it
+    }
+   , getPath: function ( $item ) {}                                                               // Set your own key-storing strategy per field
+   , onRetrieve: function ( $item, storedVal ) {}                                                 // This function will be triggered each time Garlic find an retrieve a local stored data for a field
+  }
+
+  /* GARLIC DATA-API
+  * =============== */
+  $( window ).on( 'load', function () {
+    $( '[data-persist="garlic"]' ).each( function () {
+      $(this).garlic();
+    })
+  });
+
+// This plugin works with jQuery or Zepto (with data extension builded for Zepto. See changelog 0.0.6)
+}(window.jQuery || window.Zepto);
diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php
index 63b4189..aff7281 100644
--- a/core/modules/node/lib/Drupal/node/NodeFormController.php
+++ b/core/modules/node/lib/Drupal/node/NodeFormController.php
@@ -56,8 +56,11 @@ protected function prepareEntity(EntityInterface $node) {
    * Overrides Drupal\Core\Entity\EntityFormController::form().
    */
   public function form(array $form, array &$form_state, EntityInterface $node) {
-
     $user_config = config('user.settings');
+
+    // Let the node form use localStorage to prevent data loss.
+    $form['#autosave'] = isset($node->changed) ? $node->changed : TRUE;
+
     // Some special stuff when previewing a node.
     if (isset($form_state['node_preview'])) {
       $form['#prefix'] = $form_state['node_preview'];
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
index 36baad2..c654759 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
@@ -43,6 +43,16 @@ function setUp() {
    * Creates a "Basic page" node and verifies its consistency in the database.
    */
   function testNodeCreation() {
+    // Check that the autosaving functionality is present.
+    $this->drupalGet('node/add/page');
+    $this->assertTrue(FALSE !== strpos($this->content, 'core/misc/garlic/garlic.js'), 'garlic.js is present.');
+    $this->assertTrue(FALSE !== strpos($this->content, 'core/misc/form.autosave.js'), 'form.autosave.js is present.');
+    $expected_settings = array(
+      'page-node-form' => 1,
+    );
+    $js_settings = $this->drupalGetSettings();
+    $this->assertIdentical($expected_settings, $js_settings['formAutoSave'], 'formAutoSave JS setting for this form is 1.');
+
     // Create a node.
     $edit = array();
     $langcode = LANGUAGE_NOT_SPECIFIED;
diff --git a/core/modules/node/lib/Drupal/node/Tests/PageEditTest.php b/core/modules/node/lib/Drupal/node/Tests/PageEditTest.php
index b461492..9f9ebb5 100644
--- a/core/modules/node/lib/Drupal/node/Tests/PageEditTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/PageEditTest.php
@@ -54,6 +54,15 @@ function testPageEdit() {
     $actual_url = $this->getURL();
     $this->assertEqual($edit_url, $actual_url, 'On edit page.');
 
+    // Check that the autosaving functionality is present.
+    $this->assertTrue(FALSE !== strpos($this->content, 'core/misc/garlic/garlic.js'), 'garlic.js is present.');
+    $this->assertTrue(FALSE !== strpos($this->content, 'core/misc/form.autosave.js'), 'form.autosave.js is present.');
+    $expected_settings = array(
+      'page-node-form' => $node->changed,
+    );
+    $js_settings = $this->drupalGetSettings();
+    $this->assertIdentical($expected_settings, $js_settings['formAutoSave'], 'formAutoSave JS setting for this form is correct.');
+
     // Check that the title and body fields are displayed with the correct values.
     $active = '<span class="element-invisible">' . t('(active tab)') . '</span>';
     $link_text = t('!local-task-title!active', array('!local-task-title' => t('Edit'), '!active' => $active));
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/AutoSaveTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/AutoSaveTest.php
new file mode 100644
index 0000000..d804678
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/AutoSaveTest.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Form\AutoSaveTest.
+ */
+
+namespace Drupal\system\Tests\Form;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Test the #autosave property for expected behavior.
+ */
+class AutoSaveTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('form_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Automatic form saving',
+      'description' => 'Tests #autosave property to automatically save form data to localStorage.',
+      'group' => 'Form API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $this->web_user = $this->drupalCreateUser();
+    $this->drupalLogin($this->web_user);
+  }
+
+  /**
+   * Checks if garlic.js and form.autosave.js are included, and in proper order.
+   */
+  function checkJS($present = TRUE) {
+    $position1 = strpos($this->content, 'core/misc/garlic/garlic.js');
+    $position2 = strpos($this->content, 'core/misc/form.autosave.js');
+    if ($present) {
+      $this->assertTrue($position1 !== FALSE && $position2 !== FALSE && $position1 < $position2, 'garlic.js is included before form.autosave.js.');
+    }
+    else {
+      $this->assertTrue($position1 === FALSE && $position2 === FALSE, 'garlic.js and form.autosave.js are not included.');
+    }
+  }
+
+  /**
+   * Ensures that JS settings and files #attached for #autosave are correct.
+   */
+  function testAutoSave() {
+    // Simple #autosave === FALSE/NULL.
+    $this->drupalGet('form_test/autosave/disabled');
+    $this->checkJS(FALSE);
+    $this->assertIdentical(array(), $this->drupalGetSettings(), 'No JS settings.');
+
+    // Simple: #autosave === TRUE.
+    $this->drupalGet('form_test/autosave/enabled-simple');
+    $this->checkJS();
+    $expected_settings = array(
+      'form-test-autosave-enabled-simple-form' => 1,
+    );
+    $js_settings = $this->drupalGetSettings();
+    $this->assertIdentical($expected_settings, $js_settings['formAutoSave'], 'formAutoSave JS setting for this form is 1.');
+
+    // Advanced: #autosave === 569650842.
+    $this->drupalGet('form_test/autosave/enabled-advanced');
+    $this->checkJS();
+    $expected_settings = array(
+      'form-test-autosave-enabled-advanced-form' => 569650842,
+    );
+    $js_settings = $this->drupalGetSettings();
+    $this->assertIdentical($expected_settings, $js_settings['formAutoSave'], 'formAutoSave JS setting for this form is 569650842.');
+  }
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 83d1746..271ec9e 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1291,6 +1291,21 @@ function system_library_info() {
     ),
   );
 
+  // Drupal's localStorage-powered auto form saving, powered by Garlic.js.
+  $libraries['drupal.formAutoSave'] = array(
+    'title' => 'Drupal localStorage-powered auto form saving',
+    'version' => VERSION,
+    'js' => array(
+      'core/misc/form.autosave.js' => array('group' => JS_LIBRARY),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('system', 'drupalSettings'),
+      array('system', 'garlic'),
+    ),
+  );
+
   // Drupal's dialog component.
   $libraries['drupal.dialog'] = array(
     'title' => 'Drupal Dialog',
@@ -2014,6 +2029,19 @@ function system_library_info() {
     ),
   );
 
+  // Garlic.
+  $libraries['garlic'] = array(
+    'title' => 'garlic.js',
+    'website' => 'http://garlicjs.org/',
+    'version' => '1.2.0',
+    'js' => array(
+      'core/misc/garlic/garlic.js' => array('group' => JS_LIBRARY),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+    ),
+  );
+
   // VIE.
   $libraries['vie.core'] = array(
     'title' => 'VIE.js core (excluding services, views and xdr)',
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index f85fd6d..030ec5e 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -20,6 +20,27 @@ function form_test_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
+  $items['form_test/autosave/disabled'] = array(
+    'title' => 'Auto save disabled test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_autosave_disabled_form'),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+  $items['form_test/autosave/enabled-simple'] = array(
+    'title' => 'Auto save enabled (simple) test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_autosave_enabled_simple_form'),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+  $items['form_test/autosave/enabled-advanced'] = array(
+    'title' => 'Auto save enabled (advanced) test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('form_test_autosave_enabled_advanced_form'),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
   $items['form-test/validate'] = array(
     'title' => 'Form validation handlers test',
     'page callback' => 'drupal_get_form',
@@ -477,6 +498,36 @@ function form_test_validate_form_validate(&$form, &$form_state) {
 }
 
 /**
+ * Form constructor to test the #autosave property (disabled).
+ */
+function form_test_autosave_disabled_form($form, &$form_state) {
+  $form['test'] = array(
+    '#type' => 'textfield',
+    '#title' => 'Test',
+  );
+  return $form;
+}
+
+/**
+ * Form constructor to test the #autosave property (enabled, simple mode).
+ */
+function form_test_autosave_enabled_simple_form($form, &$form_state) {
+  $form = form_test_autosave_disabled_form($form, $form_state);
+  $form['#autosave'] = TRUE;
+  return $form;
+}
+
+/**
+ * Form constructor to test the #autosave property (enabled, advanced mode).
+ */
+function form_test_autosave_enabled_advanced_form($form, &$form_state) {
+  $form = form_test_autosave_disabled_form($form, $form_state);
+  $last_modification_timestamp = 569650842;
+  $form['#autosave'] = $last_modification_timestamp;
+  return $form;
+}
+
+/**
  * Form constructor to test the #required property.
  */
 function form_test_validate_required_form($form, &$form_state) {
