Clean up gr-file-list

+ Use gr-rest-api-interface instead of gr-ajax and gr-request.
+ Use strict equality operators.
+ Clean up tests.
+ Add hover state to non-header rows.

Change-Id: I8589dc7f9466d61928add8f2587af2b8a2a56497
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 7d69d7e..d94a828 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -16,9 +16,8 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-file-list">
   <template>
@@ -49,6 +48,9 @@
         visibility: hidden;
         width: 1.25em;
       }
+      .row:not(.header):hover {
+        background-color: #f5fafd;
+      }
       .row[selected] {
         background-color: #ebf5fb;
       }
@@ -112,14 +114,6 @@
         }
       }
     </style>
-    <gr-ajax id="filesXHR"
-        url="[[_computeFilesURL(changeNum, patchNum)]]"
-        on-response="_handleResponse"></gr-ajax>
-    <gr-ajax id="reviewedXHR"
-        url="[[_computeReviewedURL(changeNum, patchNum)]]"
-        last-response="{{_reviewed}}"></gr-ajax>
-    </gr-ajax>
-
     <div class="row header">
       <div class="positionIndicator"></div>
       <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
@@ -128,7 +122,7 @@
       <div class="comments">Comments</div>
       <div class="stats">Stats</div>
     </div>
-    <template is="dom-repeat" items="{{files}}" as="file">
+    <template is="dom-repeat" items="{{_files}}" as="file">
       <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
         <div class="positionIndicator">&#x25b6;</div>
         <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
@@ -151,6 +145,7 @@
         </div>
       </div>
     </template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-file-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index ead5d74..947d982 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -24,7 +24,6 @@
       changeNum: String,
       comments: Object,
       drafts: Object,
-      files: Array,
       selectedIndex: {
         type: Number,
         notify: true,
@@ -33,6 +32,8 @@
         type: Object,
         value: function() { return document.body; },
       },
+
+      _files: Array,
       _loggedIn: {
         type: Boolean,
         value: false,
@@ -41,34 +42,34 @@
         type: Array,
         value: function() { return []; },
       },
-      _filesRequestPromise: Object,  // Used for testing.
-      _reviewedRequestPromise: Object,  // Used for testing.
-      _xhrPromise: Object,  // Used for testing.
     },
 
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
-      Gerrit.RESTClientBehavior,
     ],
 
     reload: function() {
       if (!this.changeNum || !this.patchNum) {
         return Promise.resolve();
       }
-      return Promise.all([
-        this._filesRequestPromise =
-            this.$.filesXHR.generateRequest().completes,
-        app.accountReady.then(function() {
-          this._loggedIn = app.loggedIn;
-          if (!app.loggedIn) { return; }
-          this._reviewedRequestPromise =
-              this.$.reviewedXHR.generateRequest().completes;
-        }.bind(this)),
-      ]);
-    },
 
-    _computeFilesURL: function(changeNum, patchNum) {
-      return this.changeBaseURL(changeNum, patchNum) + '/files';
+      var promises = [];
+      var _this = this;
+
+      promises.push(this._getFiles().then(function(files) {
+        _this._files = files;
+      }));
+      promises.push(this._getLoggedIn().then(function(loggedIn) {
+        return _this._loggedIn = loggedIn;
+      }).then(function(loggedIn) {
+        if (!loggedIn) { return; }
+
+        _this._getReviewedFiles().then(function(reviewed) {
+          _this._reviewed = reviewed;
+        });
+      }));
+
+      return Promise.all(promises);
     },
 
     _computeCommentsString: function(comments, patchNum, path) {
@@ -83,53 +84,63 @@
       if (!comments) { return ''; }
 
       var patchComments = (comments[path] || []).filter(function(c) {
-        return c.patch_set == patchNum;
+        return parseInt(c.patch_set, 10) === parseInt(patchNum, 10);
       });
       var num = patchComments.length;
-      if (num == 0) { return ''; }
+      if (num === 0) { return ''; }
       return num + ' ' + noun + (num > 1 ? 's' : '');
     },
 
-    _computeReviewedURL: function(changeNum, patchNum) {
-      return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed';
-    },
-
     _computeReviewed: function(file, _reviewed) {
-      return _reviewed.indexOf(file.__path) != -1;
+      return _reviewed.indexOf(file.__path) !== -1;
     },
 
     _handleReviewedChange: function(e) {
       var path = Polymer.dom(e).rootTarget.getAttribute('data-path');
       var index = this._reviewed.indexOf(path);
-      var reviewed = index != -1;
+      var reviewed = index !== -1;
       if (reviewed) {
         this.splice('_reviewed', index, 1);
       } else {
         this.push('_reviewed', path);
       }
 
-      var method = reviewed ? 'DELETE' : 'PUT';
-      var url = this.changeBaseURL(this.changeNum, this.patchNum) +
-          '/files/' + encodeURIComponent(path) + '/reviewed';
-      this._send(method, url).catch(function(err) {
+      this._saveReviewedState(path, !reviewed).catch(function(err) {
         alert('Couldn’t change file review status. Check the console ' +
             'and contact the PolyGerrit team for assistance.');
         throw err;
       }.bind(this));
     },
 
-    _handleResponse: function(e, req) {
-      var result = e.detail.response;
-      var paths = Object.keys(result).sort();
+    _saveReviewedState: function(path, reviewed) {
+      return this.$.restAPI.saveFileReviewed(this.changeNum, this.patchNum,
+          path, reviewed);
+    },
+
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
+    },
+
+    _getReviewedFiles: function() {
+      return this.$.restAPI.getReviewedFiles(this.changeNum, this.patchNum);
+    },
+
+    _getFiles: function() {
+      return this.$.restAPI.getChangeFiles(this.changeNum, this.patchNum).then(
+          this._normalizeFilesResponse.bind(this));
+    },
+
+    _normalizeFilesResponse: function(response) {
+      var paths = Object.keys(response).sort();
       var files = [];
       for (var i = 0; i < paths.length; i++) {
-        var info = result[paths[i]];
+        var info = response[paths[i]];
         info.__path = paths[i];
         info.lines_inserted = info.lines_inserted || 0;
         info.lines_deleted = info.lines_deleted || 0;
         files.push(info);
       }
-      this.files = files;
+      return files;
     },
 
     _handleKey: function(e) {
@@ -139,7 +150,7 @@
         case 74:  // 'j'
           e.preventDefault();
           this.selectedIndex =
-              Math.min(this.files.length - 1, this.selectedIndex + 1);
+              Math.min(this._files.length - 1, this.selectedIndex + 1);
           break;
         case 75:  // 'k'
           e.preventDefault();
@@ -147,7 +158,7 @@
           break;
         case 219:  // '['
           e.preventDefault();
-          this._openSelectedFile(this.files.length - 1);
+          this._openSelectedFile(this._files.length - 1);
           break;
         case 221:  // ']'
           e.preventDefault();
@@ -166,11 +177,11 @@
         this.selectedIndex = opt_index;
       }
       page.show(this._computeDiffURL(this.changeNum, this.patchNum,
-          this.files[this.selectedIndex].__path));
+          this._files[this.selectedIndex].__path));
     },
 
     _computeFileSelected: function(index, selectedIndex) {
-      return index == selectedIndex;
+      return index === selectedIndex;
     },
 
     _computeFileStatus: function(status) {
@@ -182,24 +193,15 @@
     },
 
     _computeFileDisplayName: function(path) {
-      return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+      return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path;
     },
 
     _computeClass: function(baseClass, path) {
       var classes = [baseClass];
-      if (path == COMMIT_MESSAGE_PATH) {
+      if (path === COMMIT_MESSAGE_PATH) {
         classes.push('invisible');
       }
       return classes.join(' ');
     },
-
-    _send: function(method, url) {
-      var xhr = document.createElement('gr-request');
-      this._xhrPromise = xhr.send({
-        method: method,
-        url: url,
-      });
-      return this._xhrPromise;
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 48e8cc7..21263c0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -36,204 +36,93 @@
 <script>
   suite('gr-file-list tests', function() {
     var element;
-    var server;
+    var getLoggedInStub;
 
     setup(function() {
       element = fixture('basic');
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/1/files',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({
-            '/COMMIT_MSG': {
-              status: 'A',
-              lines_inserted: 9,
-              size_delta: 317,
-              size: 317
-            },
-            'myfile.txt': {
-              lines_inserted: 35,
-              size_delta: 1146,
-              size: 1167
-            }
-          }),
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/2/files',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({
-            '/COMMIT_MSG': {
-              status: 'A',
-              lines_inserted: 9,
-              size_delta: 317,
-              size: 317
-            },
-            'myfile.txt': {
-              lines_inserted: 35,
-              size_delta: 1146,
-              size: 1167
-            },
-            'file_added_in_rev2.txt': {
-              lines_inserted: 98,
-              size_delta: 234,
-              size: 136
-            }
-          }),
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/1/drafts',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '{}',
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/2/drafts',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '{}',
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/1/files?reviewed',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '["/COMMIT_MSG"]',
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/2/files?reviewed',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '["/COMMIT_MSG","myfile.txt"]',
-        ]
-      );
-      server.respondWith(
-        'PUT',
-        '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
-        [
-          201,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '""',
-        ]
-      );
-      server.respondWith(
-        'DELETE',
-        '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
-        [
-          204,
-          {'Content-Type': 'application/json'},
-          '',
-        ]
-      );
-
-      app.loggedIn = true;
+      getLoggedInStub = sinon.stub(element, '_getLoggedIn', function() {
+        return Promise.resolve(true);
+      });
     });
 
     teardown(function() {
-      server.restore();
+      getLoggedInStub.restore();
     });
 
-    test('requests', function(done) {
-      element.changeNum = '42';
-      element.patchNum = '1';
-      element.reload();
-      server.respond();
-
-      Promise.all([
-          element._filesRequestPromise,
-          element._reviewedRequestPromise,
-      ]).then(function() {
-        flushAsynchronousOperations();
-        var filenames = element.files.map(function(f) {
-          return f.__path;
-        });
-        assert.deepEqual(filenames, ['/COMMIT_MSG', 'myfile.txt']);
-        assert.deepEqual(element._reviewed, ['/COMMIT_MSG']);
-
-        element.patchNum = '2';
-        element.reload();
-        server.respond();
-        Promise.all([
-            element._filesRequestPromise,
-            element._reviewedRequestPromise,
-        ]).then(function() {
-          flushAsynchronousOperations();
-          filenames = element.files.map(function(f) {
-            return f.__path;
+    test('get file list', function(done) {
+      var getChangeFilesStub = sinon.stub(element.$.restAPI, 'getChangeFiles',
+          function() {
+            return Promise.resolve({
+              '/COMMIT_MSG': {lines_inserted: 9},
+              'tags.html': {lines_deleted: 123},
+              'about.txt': {},
+            });
           });
-          assert.deepEqual(filenames,
-              ['/COMMIT_MSG', 'file_added_in_rev2.txt', 'myfile.txt']);
-          assert.deepEqual(element._reviewed, ['/COMMIT_MSG', 'myfile.txt']);
-          done();
+
+      element._getFiles().then(function(files) {
+        var filenames = files.map(function(f) { return f.__path; });
+        assert.deepEqual(filenames, ['/COMMIT_MSG', 'about.txt', 'tags.html']);
+        assert.deepEqual(files[0], {
+          lines_inserted: 9,
+          lines_deleted: 0,
+          __path: '/COMMIT_MSG',
         });
-      });
-    });
+        assert.deepEqual(files[1], {
+          lines_inserted: 0,
+          lines_deleted: 0,
+          __path: 'about.txt',
+        });
+        assert.deepEqual(files[2], {
+          lines_inserted: 0,
+          lines_deleted: 123,
+          __path: 'tags.html',
+        });
 
-    test('keyboard shortcuts', function(done) {
-      element.changeNum = '42';
-      element.patchNum = '2';
-      element.selectedIndex = 0;
-      element.reload();
-      server.respond();
-
-      element._filesRequestPromise.then(function() {
-        flushAsynchronousOperations();
-        var elementItems = Polymer.dom(element.root).querySelectorAll(
-            '.row:not(.header)');
-        assert.equal(elementItems.length, 3);
-        assert.isTrue(elementItems[0].hasAttribute('selected'));
-        assert.isFalse(elementItems[1].hasAttribute('selected'));
-        assert.isFalse(elementItems[2].hasAttribute('selected'));
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
-        assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
-
-        var showStub = sinon.stub(page, 'show');
-        assert.equal(element.selectedIndex, 2);
-        MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'enter'
-        assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
-            'Should navigate to /c/42/2/myfile.txt');
-
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'o'
-        assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
-            'Should navigate to /c/42/2/file_added_in_rev2.txt');
-
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        assert.equal(element.selectedIndex, 0);
-
-        showStub.restore();
         done();
       });
     });
 
+    test('keyboard shortcuts', function() {
+      element._files = [
+        {__path: '/COMMIT_MSG'},
+        {__path: 'file_added_in_rev2.txt'},
+        {__path: 'myfile.txt'},
+      ];
+      element.changeNum = '42',
+      element.patchNum = '2';
+      element.selectedIndex = 0;
+
+      flushAsynchronousOperations();
+      var elementItems = Polymer.dom(element.root).querySelectorAll(
+          '.row:not(.header)');
+      assert.equal(elementItems.length, 3);
+      assert.isTrue(elementItems[0].hasAttribute('selected'));
+      assert.isFalse(elementItems[1].hasAttribute('selected'));
+      assert.isFalse(elementItems[2].hasAttribute('selected'));
+      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+      assert.equal(element.selectedIndex, 1);
+      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+
+      var showStub = sinon.stub(page, 'show');
+      assert.equal(element.selectedIndex, 2);
+      MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'enter'
+      assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
+          'Should navigate to /c/42/2/myfile.txt');
+
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      assert.equal(element.selectedIndex, 1);
+      MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'o'
+      assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
+          'Should navigate to /c/42/2/file_added_in_rev2.txt');
+
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      assert.equal(element.selectedIndex, 0);
+
+      showStub.restore();
+    });
+
     test('comment filtering', function() {
       var comments = {
         '/COMMIT_MSG': [
@@ -284,50 +173,37 @@
           'clazz invisible');
     });
 
-    test('file review status', function(done) {
+    test('file review status', function() {
+      element._files = [
+        {__path: '/COMMIT_MSG'},
+        {__path: 'file_added_in_rev2.txt'},
+        {__path: 'myfile.txt'},
+      ];
+      element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
       element.changeNum = '42';
       element.patchNum = '2';
-      element.reload();
-      server.respond();
+      element.selectedIndex = 0;
 
-      Promise.all([
-          element._filesRequestPromise,
-          element._reviewedRequestPromise,
-      ]).then(function() {
-        flushAsynchronousOperations();
-        var fileRows =
-            Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
-        var commitMsg = fileRows[0].querySelector('input[type="checkbox"]');
-        var fileAdded = fileRows[1].querySelector('input[type="checkbox"]');
-        var myFile = fileRows[2].querySelector('input[type="checkbox"]');
+      flushAsynchronousOperations();
+      var fileRows =
+          Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+      var commitMsg = fileRows[0].querySelector('input[type="checkbox"]');
+      var fileAdded = fileRows[1].querySelector('input[type="checkbox"]');
+      var myFile = fileRows[2].querySelector('input[type="checkbox"]');
 
-        assert.isTrue(commitMsg.checked);
-        assert.isFalse(fileAdded.checked);
-        assert.isTrue(myFile.checked);
+      assert.isTrue(commitMsg.checked);
+      assert.isFalse(fileAdded.checked);
+      assert.isTrue(myFile.checked);
 
-        assert.equal(element._reviewed.length, 2);
+      var saveStub = sinon.stub(element, '_saveReviewedState',
+          function() { return Promise.resolve(); });
 
-        MockInteractions.tap(commitMsg);
-        server.respond();
-        element._xhrPromise.then(function(req) {
-          assert.equal(element._reviewed.length, 1);
-          assert.equal(req.status, 204);
-          assert.equal(req.url,
-              '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
+      MockInteractions.tap(commitMsg);
+      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
+      MockInteractions.tap(commitMsg);
+      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
 
-          MockInteractions.tap(commitMsg);
-          server.respond();
-        }).then(function() {
-          element._xhrPromise.then(function(req) {
-            assert.equal(element._reviewed.length, 2);
-            assert.equal(req.status, 201);
-            assert.equal(req.url,
-                '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
-
-            done();
-          });
-        });
-      });
+      saveStub.restore();
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 72570d9..85d26e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -130,6 +130,48 @@
       return this._sharedFetchPromises[url];
     },
 
+    getChangeFiles: function(changeNum, patchNum) {
+      return this.fetchJSON(
+          this._changeBaseURL(changeNum, patchNum) + '/files');
+    },
+
+    getReviewedFiles: function(changeNum, patchNum) {
+      return this.fetchJSON(
+          this._changeBaseURL(changeNum, patchNum) + '/files?reviewed');
+    },
+
+    saveFileReviewed: function(changeNum, patchNum, path, reviewed, opt_errFn,
+        opt_ctx) {
+      var method = reviewed ? 'PUT' : 'DELETE';
+      var url = this._changeBaseURL(changeNum, patchNum) + '/files/' +
+          encodeURIComponent(path) + '/reviewed';
+
+      return this._save(method, url, null, opt_errFn, opt_ctx);
+    },
+
+    _save: function(method, url, opt_body, opt_errFn, opt_ctx) {
+      var headers = new Headers({
+        'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'),
+      });
+
+      if (opt_body) {
+        headers.append('Content-Type', 'application/json');
+        options.body = body;
+      }
+      var options = {
+        method: method,
+        headers: headers,
+        credentials: 'same-origin',
+      };
+      return fetch(url, options).catch(function(err) {
+        if (opt_errFn) {
+          opt_errFn.call(opt_ctx || this);
+        } else {
+          throw err;
+        }
+      });
+    },
+
     getDiff: function(changeNum, basePatchNum, patchNum, path,
         opt_cancelCondition) {
       var url = this._getDiffFetchURL(changeNum, patchNum, path);
@@ -165,7 +207,7 @@
         opt_patchNum, opt_path) {
       if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
         return this.fetchJSON(
-            this._getDiffCommentsFetchURL(changeNum, null, '/drafts'));
+            this._getDiffCommentsFetchURL(changeNum, '/drafts'));
       }
 
       function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
@@ -212,5 +254,20 @@
       return v;
     },
 
+    _getCookie: function(name) {
+      var key = name + '=';
+      var cookies = document.cookie.split(';');
+      for (var i = 0; i < cookies.length; i++) {
+        var c = cookies[i];
+        while (c.charAt(0) == ' ') {
+          c = c.substring(1);
+        }
+        if (c.indexOf(key) == 0) {
+          return c.substring(key.length, c.length);
+        }
+      }
+      return '';
+    },
+
   });
 })();