<commit_before>/* global by, element */
'use strict';

var authentication = require('../authentication/authentication');
var common = require('../common/common');
var navigation = require('../common/navigation');
var components = require('../components/components');

var blockStorageComponent = {
  id: 'tosca.nodes.BlockStorage:2.0',
  elementId: 'tosca.nodes.BlockStorage',
  archiveVersion: '2.0'
};
var computeComponent = {
  id: 'tosca.nodes.Compute:1.0',
  elementId: 'tosca.nodes.Compute',
  archiveVersion: '1.0'
};
var computeComponentV2 = {
  id: 'tosca.nodes.Compute:2.0',
  elementId: 'tosca.nodes.Compute',
  archiveVersion: '2.0'
};
var rootCapability = 'feature';
var rootCapabilityType = 'tosca.capabilities.Feature';
var computeCapability = 'compute';

var checkRecommanded = function(recommended, capabilityRow) {
  expect(capabilityRow.element(by.css('.alert-success')).isPresent()).toBe(recommended);
  expect(capabilityRow.element(by.css('a.btn-success')).isDisplayed()).toBe(!recommended);
  expect(capabilityRow.element(by.css('a.btn-danger')).isDisplayed()).toBe(recommended);
};

var findCapabilityRow = function(testedCapabilityId) {
  return browser.element(by.id(testedCapabilityId));
};

var flagComponentAsRecommanded = function(component, testedCapability) {
  components.goToComponentDetailPage(component.id);
  expect(element(by.binding('component.elementId')).getText()).toContain(component.elementId);
  expect(element(by.binding('component.archiveVersion')).getText()).toContain(component.archiveVersion);

  var firstCapaRow = findCapabilityRow(testedCapability);
  expect(firstCapaRow.getText()).toContain(testedCapability);
  expect(firstCapaRow.isElementPresent(by.css('a.btn-success'))).toBe(true);
  checkRecommanded(false, firstCapaRow);
  var recommendButton = firstCapaRow.element(by.css('a.btn-success'));
  // recommend for this capability
  recommendButton.click();
};

describe('Component Details :', function() {
  // Load up a view and wait for it to be done with its rendering and epicycles.
  beforeEach(function() {
    common.before();
    // Login
    authentication.login('componentManager');
    navigation.go('main', 'components');
  });

  afterEach(function() {
    // Logout action
    common.after();
  });

  it('should be able to go to component search page, upload a csar, list components and check pagination', function() {
    console.log('################# should be able to go to component search page, upload a csar, list components and check pagination');
    // the serach side panel
    expect(element(by.id('comp-search-side-pannel')).isPresent()).toBe(true);
    // the search result panel
    expect(element(by.id('comp-search-result-panel')).isPresent()).toBe(true);

    var results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(20);

    // pagination
    var pagination = element.all(by.repeater('page in pages'));
    expect(pagination.count()).toEqual(6);

    // go to the second page and check
    pagination.get(3).element(by.tagName('a')).click();
    results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(6);
  });

  it('should be able to have components grouped by version when there are multiple versions of the same component', function() {
    console.log('################# should be able to have components grouped by version when there are multiple versions of the same component');
    var results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(20);

    // pagination
    var pagination = element.all(by.repeater('page in pages'));
    expect(pagination.count()).toEqual(6);

    // go to the second page and check
    pagination.get(3).element(by.tagName('a')).click();
    results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(6);
    pagination.get(0).element(by.tagName('a')).click();
    browser.waitForAngular();
    components.goToComponentDetailPage(computeComponentV2.id);
    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponentV2.elementId);
    expect(element(by.binding('component.archiveVersion')).getText()).toContain(computeComponentV2.archiveVersion);

    navigation.go('main', 'components');
    components.changeComponentVersionAndGo(computeComponentV2.id, computeComponent.archiveVersion);
    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponent.elementId);
    expect(element(by.binding('component.archiveVersion')).getText()).toContain(computeComponent.archiveVersion);
  });

  it('should be able to see details of the component [' + blockStorageComponent.id + '], and recommend that component as default for a capability ', function() {
    console.log('################# should be able to see details of the component [' + blockStorageComponent.id + '], and recommend that component as default for a capability ');
    flagComponentAsRecommanded(blockStorageComponent, rootCapability);
    checkRecommanded(true, findCapabilityRow(rootCapability));
  });

  it('should be able to trigger recommendation of another component [' + blockStorageComponent.id + '] as default for an already recommended capability: cancel and confirm ', function() {
    console.log('################# should be able to trigger recommendation of another component [' + blockStorageComponent.id   + '] as default for an already recommended capability: cancel and confirm ');
    navigation.go('main', 'components');
    var pagination = element.all(by.repeater('page in pages'));
    pagination.last().element(by.tagName('a')).click();
    components.goToComponentDetailPage(blockStorageComponent.id);
    checkRecommanded(true, findCapabilityRow(rootCapability));

    components.goToComponentDetailPage(computeComponentV2.id);
    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponentV2.elementId);

    // first be sure it is not recommended yet
    var firstCapaRow = findCapabilityRow(rootCapability);
    expect(firstCapaRow.getText()).toContain(rootCapability);
    checkRecommanded(false, firstCapaRow);

    var recommendButton = firstCapaRow.element(by.css('a.btn-success'));

    // case cancel
    // trigger for recommendation
    recommendButton.click();
    expect(element(by.className('modal-body')).getText()).toContain(blockStorageComponent.id);
    expect(element(by.className('modal-body')).getText()).toContain(rootCapabilityType);
    element(by.binding('CANCEL')).click();
    checkRecommanded(false, firstCapaRow);

    // case confirm
    // trigger for recommendation
    recommendButton.click();
    expect(element(by.className('modal-body')).getText()).toContain(blockStorageComponent.id);
    expect(element(by.className('modal-body')).getText()).toContain(rootCapabilityType);
    element(by.binding('COMPONENTS.CONFIRM_RECOMMENDATION_MODAL.OK')).click();
    checkRecommanded(true, findCapabilityRow(rootCapability));
  });

  it('should be able to undefine the component [' + computeComponentV2.id + '], as default for a capability ', function() {
    console.log('################# should be able to undefine the component [' + computeComponentV2.id + '], as default for a capability ');
    navigation.go('main', 'components');
    var pagination = element.all(by.repeater('page in pages'));
    browser.actions().click(pagination.last().element(by.tagName('a'))).perform();
    components.goToComponentDetailPage(computeComponentV2.id);

    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponentV2.elementId);

    flagComponentAsRecommanded(computeComponentV2, computeCapability);
    // first be sure it is already recommended
    var firstCapaRow = findCapabilityRow(computeCapability);
    expect(firstCapaRow.getText()).toContain(computeCapability);
    expect(firstCapaRow.isElementPresent(by.css('a.btn-danger'))).toBe(true);
    checkRecommanded(true, findCapabilityRow(computeCapability));
    var undefinedDefaultButton = firstCapaRow.element(by.css('a.btn-danger'));

    // undefine the component as default
    undefinedDefaultButton.click();
    checkRecommanded(false, findCapabilityRow(computeCapability));
  });

});
<commit_msg>fixed ui tests<commit_after>/* global by, element */
'use strict';

var authentication = require('../authentication/authentication');
var common = require('../common/common');
var navigation = require('../common/navigation');
var components = require('../components/components');

var blockStorageComponent = {
  id: 'tosca.nodes.BlockStorage:2.0',
  elementId: 'tosca.nodes.BlockStorage',
  archiveVersion: '2.0'
};
var computeComponent = {
  id: 'tosca.nodes.Compute:1.0',
  elementId: 'tosca.nodes.Compute',
  archiveVersion: '1.0'
};
var computeComponentV2 = {
  id: 'tosca.nodes.Compute:2.0',
  elementId: 'tosca.nodes.Compute',
  archiveVersion: '2.0'
};
var rootCapability = 'feature';
var rootCapabilityType = 'tosca.capabilities.Feature';
var computeCapability = 'compute';

var checkRecommanded = function(recommended, capabilityRow) {
  expect(capabilityRow.element(by.css('.alert-success')).isPresent()).toBe(recommended);
  expect(capabilityRow.element(by.css('a.btn-success')).isDisplayed()).toBe(!recommended);
  expect(capabilityRow.element(by.css('a.btn-danger')).isDisplayed()).toBe(recommended);
};

var findCapabilityRow = function(testedCapabilityId) {
  return browser.element(by.id(testedCapabilityId));
};

var flagComponentAsRecommanded = function(component, testedCapability) {
  components.goToComponentDetailPage(component.id);
  expect(element(by.binding('component.elementId')).getText()).toContain(component.elementId);
  expect(element(by.binding('component.archiveVersion')).getText()).toContain(component.archiveVersion);

  var firstCapaRow = findCapabilityRow(testedCapability);
  expect(firstCapaRow.getText()).toContain(testedCapability);
  expect(firstCapaRow.isElementPresent(by.css('a.btn-success'))).toBe(true);
  checkRecommanded(false, firstCapaRow);
  var recommendButton = firstCapaRow.element(by.css('a.btn-success'));
  // recommend for this capability
  recommendButton.click();
};

describe('Component Details :', function() {
  // Load up a view and wait for it to be done with its rendering and epicycles.
  beforeEach(function() {
    common.before();
    // Login
    authentication.login('componentManager');
    navigation.go('main', 'components');
  });

  afterEach(function() {
    // Logout action
    common.after();
  });

  it('should be able to go to component search page, upload a csar, list components and check pagination', function() {
    console.log('################# should be able to go to component search page, upload a csar, list components and check pagination');
    // the serach side panel
    expect(element(by.id('comp-search-side-pannel')).isPresent()).toBe(true);
    // the search result panel
    expect(element(by.id('comp-search-result-panel')).isPresent()).toBe(true);

    var results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(20);

    // pagination
    var pagination = element.all(by.repeater('page in pages'));
    expect(pagination.count()).toEqual(6);

    // go to the second page and check
    pagination.get(3).element(by.tagName('a')).click();
    results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(7);
  });

  it('should be able to have components grouped by version when there are multiple versions of the same component', function() {
    console.log('################# should be able to have components grouped by version when there are multiple versions of the same component');
    var results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(20);

    // pagination
    var pagination = element.all(by.repeater('page in pages'));
    expect(pagination.count()).toEqual(6);

    // go to the second page and check
    pagination.get(3).element(by.tagName('a')).click();
    results = element.all(by.repeater('component in searchResult.data.data'));
    expect(results.count()).toEqual(7);
    pagination.get(0).element(by.tagName('a')).click();
    browser.waitForAngular();
    components.goToComponentDetailPage(computeComponentV2.id);
    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponentV2.elementId);
    expect(element(by.binding('component.archiveVersion')).getText()).toContain(computeComponentV2.archiveVersion);

    navigation.go('main', 'components');
    components.changeComponentVersionAndGo(computeComponentV2.id, computeComponent.archiveVersion);
    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponent.elementId);
    expect(element(by.binding('component.archiveVersion')).getText()).toContain(computeComponent.archiveVersion);
  });

  it('should be able to see details of the component [' + blockStorageComponent.id + '], and recommend that component as default for a capability ', function() {
    console.log('################# should be able to see details of the component [' + blockStorageComponent.id + '], and recommend that component as default for a capability ');
    flagComponentAsRecommanded(blockStorageComponent, rootCapability);
    checkRecommanded(true, findCapabilityRow(rootCapability));
  });

  it('should be able to trigger recommendation of another component [' + blockStorageComponent.id + '] as default for an already recommended capability: cancel and confirm ', function() {
    console.log('################# should be able to trigger recommendation of another component [' + blockStorageComponent.id   + '] as default for an already recommended capability: cancel and confirm ');
    navigation.go('main', 'components');
    var pagination = element.all(by.repeater('page in pages'));
    pagination.last().element(by.tagName('a')).click();
    components.goToComponentDetailPage(blockStorageComponent.id);
    checkRecommanded(true, findCapabilityRow(rootCapability));

    components.goToComponentDetailPage(computeComponentV2.id);
    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponentV2.elementId);

    // first be sure it is not recommended yet
    var firstCapaRow = findCapabilityRow(rootCapability);
    expect(firstCapaRow.getText()).toContain(rootCapability);
    checkRecommanded(false, firstCapaRow);

    var recommendButton = firstCapaRow.element(by.css('a.btn-success'));

    // case cancel
    // trigger for recommendation
    recommendButton.click();
    expect(element(by.className('modal-body')).getText()).toContain(blockStorageComponent.id);
    expect(element(by.className('modal-body')).getText()).toContain(rootCapabilityType);
    element(by.binding('CANCEL')).click();
    checkRecommanded(false, firstCapaRow);

    // case confirm
    // trigger for recommendation
    recommendButton.click();
    expect(element(by.className('modal-body')).getText()).toContain(blockStorageComponent.id);
    expect(element(by.className('modal-body')).getText()).toContain(rootCapabilityType);
    element(by.binding('COMPONENTS.CONFIRM_RECOMMENDATION_MODAL.OK')).click();
    checkRecommanded(true, findCapabilityRow(rootCapability));
  });

  it('should be able to undefine the component [' + computeComponentV2.id + '], as default for a capability ', function() {
    console.log('################# should be able to undefine the component [' + computeComponentV2.id + '], as default for a capability ');
    navigation.go('main', 'components');
    var pagination = element.all(by.repeater('page in pages'));
    browser.actions().click(pagination.last().element(by.tagName('a'))).perform();
    components.goToComponentDetailPage(computeComponentV2.id);

    expect(element(by.binding('component.elementId')).getText()).toContain(computeComponentV2.elementId);

    flagComponentAsRecommanded(computeComponentV2, computeCapability);
    // first be sure it is already recommended
    var firstCapaRow = findCapabilityRow(computeCapability);
    expect(firstCapaRow.getText()).toContain(computeCapability);
    expect(firstCapaRow.isElementPresent(by.css('a.btn-danger'))).toBe(true);
    checkRecommanded(true, findCapabilityRow(computeCapability));
    var undefinedDefaultButton = firstCapaRow.element(by.css('a.btn-danger'));

    // undefine the component as default
    undefinedDefaultButton.click();
    checkRecommanded(false, findCapabilityRow(computeCapability));
  });

});
<|endoftext|>


<commit_before>(function() {

  'use strict';

  function removeTrailingSlash(uri) {
    if (uri.lastIndexOf('/') == uri.length - 1) {
      return removeTrailingSlash(uri.substr(0, uri.length - 1));
    }
    return uri;
  }

  // http(String method, String url, Array headers (optional), TypedArray data (optional), Function callback (optional));
  function http(method, url) {
      const args = Array.prototype.slice.call(arguments, http.length);
      const fn = args.pop();
      const headers = args.length > 0 && Array.isArray(args[0]) ? args[0] : [];
      const data = args.length > 0 && !Array.isArray(args[args.length-1]) ? args[args.length-1] : null;
      const xhr = new XMLHttpRequest();
      xhr.open(method, url);
      xhr.onreadystatechange = function() {
        xhr.readyState == 4 && typeof fn === 'function' && fn(xhr);
      }
      headers.filter(function(h) { return Array.isArray(h) && h.length == 2 })
             .forEach(function(h) { xhr.setRequestHeader(h[0], h[1]) });
      xhr.send(data);
  }

  function ready(fn) {
    if (document.readyState != 'loading'){
      fn();
    } else {
      document.addEventListener('DOMContentLoaded', fn);
    }
  }

  function listen(id, event, fn) {
    const el = document.getElementById(id);
    if (el) {
      el.addEventListener(event, fn);
    }
  }

  function addChild() {
    document.getElementById('btn_action_create').disabled = true;

    const id = document.getElementById('new_id').value.trim();
    const mixin = document.getElementById('new_mixin').value;
    const uri = removeTrailingSlash(document.getElementById('main').getAttribute('resource'));

    const method = id == '' ? 'POST' : 'PUT';
    const url = id == '' ? uri : uri + '/' + id;
    const headers = [];

    const fn = function (method, url, headers, data) {
      http(method, url, headers, data, function(res) {
        if (res.status == 201) {
          const loc = res.getResponseHeader('Location');
          const link = res.getResponseHeader('Link');
          if (link != null && link.match('rel="describedby"')) {
            const str = link.match(/<[^>]+>/)[0];
            window.location = str.slice(1, str.length - 1);
          } else if (loc != null) {
            window.location = loc;
          } else {
            window.location.reload();
          }
        } else {
            ajaxErrorHandler(res, 'Error creating binary');
        }
      });
    }

    if (mixin == 'binary') {
      const update_file = document.getElementById('binary_payload').files[0];
      const reader = new FileReader();
      headers.push(['Content-Disposition', 'attachment; filename=\"' + update_file.name + '\"']);
      headers.push(['Content-Type', update_file.type || 'application/octet-stream']);
      reader.onload = function(e) {
          fn(method, url, headers, e.target.result);
      };
      reader.readAsArrayBuffer(update_file);
    } else {
      const turtle = document.getElementById('turtle_payload');
      if (turtle && turtle.value) {
        headers.push(['Content-Type', 'text/turtle']);
        fn(method, url, headers, turtle.value);
      } else {
        fn(method, url, headers, null);
      }
    }
  }

  function checkIfNonRdfResource(e) {
      const url = this.href;
      http('HEAD', url, function(res) {
        if (res.status >= 400 || res.getResponseHeader('Link') == null) {
          var newLocation = url;
          // Note: HEADing an external resource returns a temporary redirect to the external resource:
          // therefore there is no Link header. However what we want to see is the metadata
          // for the external reference rather than  the external object itself.
          // (c.f. https://jira.duraspace.org/browse/FCREPO-2387)
          // WARNING: Fragile code relying on magic suffix '/fcr:metadata' and absence of 'Link' header
          // on external resource.
          if(!url.match(/.*fcr:metadata/)){
            newLocation = url + "/fcr:metadata";
          }
          location.href = newLocation ;
          return;
        }

        // Note: this Link header parsing works for the reference implementation, but it is not particularly robust
        const headers = res.getResponseHeader('Link').split(', ');
        const isNonRdfSource = headers.filter(function(h) { return h.match(/rel=["']?type["']?/) })
                                      .some(function(h) { return h.match(/NonRDFSource/) });

        if (isNonRdfSource) {
            const description = headers.filter(function(h) { return h.match(/rel="describedby"/)});
            if (description.length > 0) {
                location.href = description[0].substr(1, description[0].indexOf('>') - 1);
                return;
            }
        }

        location.href = url;
      });
      e.preventDefault();
  }

  function submitAndFollowLocation(e) {
      http('POST', e.target.getAttribute('action'), 'data', function(res) {
        if (res.status == 201 || res.status == 204) {
          window.location = res.getResponseHeader('Location');
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function removeVersion(e) {
      const redirect = e.target.dataset.redirectAfterSubmit;
      http('DELETE', e.target.getAttribute('action'), function(res) {
        if (res.status == 204) {
          window.location = redirect;
        } else {
          ajaxErrorHandler(res, 'Error removing version');
        }
      });
      e.preventDefault();
  }

  function patchAndReload(e) {
      const redirect = e.target.dataset.redirectAfterSubmit;
      http('PATCH', e.target.getAttribute('action'), function(res) {
        if (res.status >= 400) {
          ajaxErrorHandler(res);
        } else {
          window.location = redirect;
        }
      });
      e.preventDefault();
  }

  function submitAndRedirectToBase(e) {
      const redirect = e.target.dataset.redirectAfterSubmit;
      http('POST', e.target.getAttribute('action'), function(res) {
        if (res.status >= 400) {
          ajaxErrorHandler(res);
        } else {
          window.location = redirect;
        }
      });
      e.preventDefault();
  }

  function sendSparqlUpdate(e) {
      const data = document.getElementById('sparql_update_query').value;
      http('PATCH', window.location, [['Content-Type', 'application/sparql-update']], data, function(res) {
        if (res.status == 204) {
          window.location.reload(true);
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function createVersionSnapshot(e) {
      const uri = document.getElementById('main').getAttribute('resource');
      var name = document.getElementById('version_id').value.trim();
      if (!name) {
          const d = new Date();
          name = 'version.' + d.getFullYear().toString() + (d.getMonth()+1).toString() + d.getDate().toString() + d.getHours() + d.getMinutes() + d.getSeconds();
      }

      http('POST', uri + '/fcr:versions', [['Slug', name]], function(res) {
        if (res.status == 201) {
          window.location = uri + '/fcr:versions';
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function deleteItem(e) {
      const uri = document.getElementById('main').getAttribute('resource');
      const arr = uri.toString().split('/');
      arr.pop();

      http('DELETE', uri, function(res) {
        if (res.status == 204) {
          window.location = arr.join('/');
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function updateFile(e) {
      const update_file = document.getElementById('update_file').files[0];
      if (!update_file) {
        return;
      }

      document.getElementById('binary_update_content').disabled = true;
      const url = window.location.href.replace('fcr:metadata', '');
      const reader = new FileReader();

      const headers = [
        ['Content-Disposition', 'attachment; filename=\"' + update_file.name + '\"'],
        ['Content-Type', update_file.type]];

      reader.onload = function(e) {
          http('PUT', url, headers, e.target.result, function(res) {
              if (res.status == 204 || res.status == 201) {
                  window.location.reload(true);
              } else {
                  ajaxErrorHandler(res, 'Error updating binary');
              }
          });
      };
      reader.readAsArrayBuffer(update_file);
      e.preventDefault();
  }

  function updateAccessRoles(e)
  {
      const update_json = document.getElementById('rbacl_json').value;
      const url = window.location + '/fcr:accessroles';
      http('POST', url, [['Content-Type', 'application/json']], update_json, function(res) {
          if (res.status == 204 || res.status == 201) {
              window.location.reload(true);
          } else {
              ajaxErrorHandler(res, 'Error');
          }
      });
      e.preventDefault();
  }

  function ajaxErrorHandler(xhr, errorThrown) {
      document.getElementById('errorLabel').textContent = errorThrown || xhr.statusText;
      document.getElementById('errorText').textContent = xhr.responseText;
      document.getElementById('showErrorModal').click();
      (document.getElementById('btn_action_create') || {}).disabled = false;
      (document.getElementById('binary_update_content') || {}).disabled = false;
  }

  ready(function() {
      listen('new_mixin', 'change', function(e) {
        document.getElementById('binary_payload_container').style.display = e.target.value == 'binary' ? 'block' : 'none';
        document.getElementById('turtle_payload_container').style.display = e.target.value == 'binary' ? 'none' : 'block';

      });

      listen('btn_action_create', 'click', function(e) {
        e.preventDefault();
        if (document.getElementById('new_mixin').value == 'binary') {
          const files = document.getElementById('binary_payload');
          if (files.value.length == 0) {
            files.click();
            return;
          }
        }
        addChild();
      });

      listen('action_sparql_update', 'submit', sendSparqlUpdate);
      listen('action_delete', 'submit', deleteItem);
      listen('action_create_transaction', 'submit', submitAndFollowLocation);
      listen('action_rollback_transaction', 'submit', submitAndRedirectToBase);
      listen('action_commit_transaction', 'submit', submitAndRedirectToBase);
      listen('action_revert', 'submit', patchAndReload);
      listen('action_remove_version', 'submit', removeVersion);
      listen('action_create_version', 'submit', createVersionSnapshot);
      listen('action_update_file', 'submit', updateFile);
      listen('update_rbacl', 'submit', updateAccessRoles);

      const links = document.querySelectorAll('a[property][href*="' + location.host + '"],#childList a,.breadcrumb a,.version_link');
      for (var i = 0; i < links.length; ++i) {
        links[i].addEventListener('click', checkIfNonRdfResource);
      }
  });
})();
<commit_msg>Correctly parse 'described-by' Link header. (FCREPO-2341) (#1184)<commit_after>(function() {

  'use strict';

  function removeTrailingSlash(uri) {
    if (uri.lastIndexOf('/') == uri.length - 1) {
      return removeTrailingSlash(uri.substr(0, uri.length - 1));
    }
    return uri;
  }

  // http(String method, String url, Array headers (optional), TypedArray data (optional), Function callback (optional));
  function http(method, url) {
      const args = Array.prototype.slice.call(arguments, http.length);
      const fn = args.pop();
      const headers = args.length > 0 && Array.isArray(args[0]) ? args[0] : [];
      const data = args.length > 0 && !Array.isArray(args[args.length-1]) ? args[args.length-1] : null;
      const xhr = new XMLHttpRequest();
      xhr.open(method, url);
      xhr.onreadystatechange = function() {
        xhr.readyState == 4 && typeof fn === 'function' && fn(xhr);
      }
      headers.filter(function(h) { return Array.isArray(h) && h.length == 2 })
             .forEach(function(h) { xhr.setRequestHeader(h[0], h[1]) });
      xhr.send(data);
  }

  function ready(fn) {
    if (document.readyState != 'loading'){
      fn();
    } else {
      document.addEventListener('DOMContentLoaded', fn);
    }
  }

  function listen(id, event, fn) {
    const el = document.getElementById(id);
    if (el) {
      el.addEventListener(event, fn);
    }
  }

  function addChild() {
    document.getElementById('btn_action_create').disabled = true;

    const id = document.getElementById('new_id').value.trim();
    const mixin = document.getElementById('new_mixin').value;
    const uri = removeTrailingSlash(document.getElementById('main').getAttribute('resource'));

    const method = id == '' ? 'POST' : 'PUT';
    const url = id == '' ? uri : uri + '/' + id;
    const headers = [];

    const fn = function (method, url, headers, data) {
      http(method, url, headers, data, function(res) {
        if (res.status == 201) {
          const loc = res.getResponseHeader('Location');
          const linkheaders = (res.getResponseHeader('Link') != null) ? res.getResponseHeader('Link').split(", ") : null;
          const link = (linkheaders != null) ? linkheaders.filter(function(h) { return h.match(/rel="describedby"/)}) : "";
          if (linkheaders != null && link.length > 0) {
            window.location = link[0].substr(1, link[0].indexOf('>') - 1);
          } else if (loc != null) {
            window.location = loc;
          } else {
            window.location.reload();
          }
        } else {
            ajaxErrorHandler(res, 'Error creating binary');
        }
      });
    }

    if (mixin == 'binary') {
      const update_file = document.getElementById('binary_payload').files[0];
      const reader = new FileReader();
      headers.push(['Content-Disposition', 'attachment; filename=\"' + update_file.name + '\"']);
      headers.push(['Content-Type', update_file.type || 'application/octet-stream']);
      reader.onload = function(e) {
          fn(method, url, headers, e.target.result);
      };
      reader.readAsArrayBuffer(update_file);
    } else {
      const turtle = document.getElementById('turtle_payload');
      if (turtle && turtle.value) {
        headers.push(['Content-Type', 'text/turtle']);
        fn(method, url, headers, turtle.value);
      } else {
        fn(method, url, headers, null);
      }
    }
  }

  function checkIfNonRdfResource(e) {
      const url = this.href;
      http('HEAD', url, function(res) {
        if (res.status >= 400 || res.getResponseHeader('Link') == null) {
          var newLocation = url;
          // Note: HEADing an external resource returns a temporary redirect to the external resource:
          // therefore there is no Link header. However what we want to see is the metadata
          // for the external reference rather than  the external object itself.
          // (c.f. https://jira.duraspace.org/browse/FCREPO-2387)
          // WARNING: Fragile code relying on magic suffix '/fcr:metadata' and absence of 'Link' header
          // on external resource.
          if(!url.match(/.*fcr:metadata/)){
            newLocation = url + "/fcr:metadata";
          }
          location.href = newLocation ;
          return;
        }

        // Note: this Link header parsing works for the reference implementation, but it is not particularly robust
        const headers = res.getResponseHeader('Link').split(', ');
        const isNonRdfSource = headers.filter(function(h) { return h.match(/rel=["']?type["']?/) })
                                      .some(function(h) { return h.match(/NonRDFSource/) });

        if (isNonRdfSource) {
            const description = headers.filter(function(h) { return h.match(/rel="describedby"/)});
            if (description.length > 0) {
                location.href = description[0].substr(1, description[0].indexOf('>') - 1);
                return;
            }
        }

        location.href = url;
      });
      e.preventDefault();
  }

  function submitAndFollowLocation(e) {
      http('POST', e.target.getAttribute('action'), 'data', function(res) {
        if (res.status == 201 || res.status == 204) {
          window.location = res.getResponseHeader('Location');
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function removeVersion(e) {
      const redirect = e.target.dataset.redirectAfterSubmit;
      http('DELETE', e.target.getAttribute('action'), function(res) {
        if (res.status == 204) {
          window.location = redirect;
        } else {
          ajaxErrorHandler(res, 'Error removing version');
        }
      });
      e.preventDefault();
  }

  function patchAndReload(e) {
      const redirect = e.target.dataset.redirectAfterSubmit;
      http('PATCH', e.target.getAttribute('action'), function(res) {
        if (res.status >= 400) {
          ajaxErrorHandler(res);
        } else {
          window.location = redirect;
        }
      });
      e.preventDefault();
  }

  function submitAndRedirectToBase(e) {
      const redirect = e.target.dataset.redirectAfterSubmit;
      http('POST', e.target.getAttribute('action'), function(res) {
        if (res.status >= 400) {
          ajaxErrorHandler(res);
        } else {
          window.location = redirect;
        }
      });
      e.preventDefault();
  }

  function sendSparqlUpdate(e) {
      const data = document.getElementById('sparql_update_query').value;
      http('PATCH', window.location, [['Content-Type', 'application/sparql-update']], data, function(res) {
        if (res.status == 204) {
          window.location.reload(true);
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function createVersionSnapshot(e) {
      const uri = document.getElementById('main').getAttribute('resource');
      var name = document.getElementById('version_id').value.trim();
      if (!name) {
          const d = new Date();
          name = 'version.' + d.getFullYear().toString() + (d.getMonth()+1).toString() + d.getDate().toString() + d.getHours() + d.getMinutes() + d.getSeconds();
      }

      http('POST', uri + '/fcr:versions', [['Slug', name]], function(res) {
        if (res.status == 201) {
          window.location = uri + '/fcr:versions';
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function deleteItem(e) {
      const uri = document.getElementById('main').getAttribute('resource');
      const arr = uri.toString().split('/');
      arr.pop();

      http('DELETE', uri, function(res) {
        if (res.status == 204) {
          window.location = arr.join('/');
        } else {
          ajaxErrorHandler(res);
        }
      });
      e.preventDefault();
  }

  function updateFile(e) {
      const update_file = document.getElementById('update_file').files[0];
      if (!update_file) {
        return;
      }

      document.getElementById('binary_update_content').disabled = true;
      const url = window.location.href.replace('fcr:metadata', '');
      const reader = new FileReader();

      const headers = [
        ['Content-Disposition', 'attachment; filename=\"' + update_file.name + '\"'],
        ['Content-Type', update_file.type]];

      reader.onload = function(e) {
          http('PUT', url, headers, e.target.result, function(res) {
              if (res.status == 204 || res.status == 201) {
                  window.location.reload(true);
              } else {
                  ajaxErrorHandler(res, 'Error updating binary');
              }
          });
      };
      reader.readAsArrayBuffer(update_file);
      e.preventDefault();
  }

  function updateAccessRoles(e)
  {
      const update_json = document.getElementById('rbacl_json').value;
      const url = window.location + '/fcr:accessroles';
      http('POST', url, [['Content-Type', 'application/json']], update_json, function(res) {
          if (res.status == 204 || res.status == 201) {
              window.location.reload(true);
          } else {
              ajaxErrorHandler(res, 'Error');
          }
      });
      e.preventDefault();
  }

  function ajaxErrorHandler(xhr, errorThrown) {
      document.getElementById('errorLabel').textContent = errorThrown || xhr.statusText;
      document.getElementById('errorText').textContent = xhr.responseText;
      document.getElementById('showErrorModal').click();
      (document.getElementById('btn_action_create') || {}).disabled = false;
      (document.getElementById('binary_update_content') || {}).disabled = false;
  }

  ready(function() {
      listen('new_mixin', 'change', function(e) {
        document.getElementById('binary_payload_container').style.display = e.target.value == 'binary' ? 'block' : 'none';
        document.getElementById('turtle_payload_container').style.display = e.target.value == 'binary' ? 'none' : 'block';

      });

      listen('btn_action_create', 'click', function(e) {
        e.preventDefault();
        if (document.getElementById('new_mixin').value == 'binary') {
          const files = document.getElementById('binary_payload');
          if (files.value.length == 0) {
            files.click();
            return;
          }
        }
        addChild();
      });

      listen('action_sparql_update', 'submit', sendSparqlUpdate);
      listen('action_delete', 'submit', deleteItem);
      listen('action_create_transaction', 'submit', submitAndFollowLocation);
      listen('action_rollback_transaction', 'submit', submitAndRedirectToBase);
      listen('action_commit_transaction', 'submit', submitAndRedirectToBase);
      listen('action_revert', 'submit', patchAndReload);
      listen('action_remove_version', 'submit', removeVersion);
      listen('action_create_version', 'submit', createVersionSnapshot);
      listen('action_update_file', 'submit', updateFile);
      listen('update_rbacl', 'submit', updateAccessRoles);

      const links = document.querySelectorAll('a[property][href*="' + location.host + '"],#childList a,.breadcrumb a,.version_link');
      for (var i = 0; i < links.length; ++i) {
        links[i].addEventListener('click', checkIfNonRdfResource);
      }
  });
})();
<|endoftext|>


<commit_before>import React from 'react';
import BaseGuide from '../components/BaseGuide';
import { InlineJS as IJS, PrismBlock } from '../components/PrismBlocks';
import { Note } from '../components/Messages';
import { Link } from '@curi/react';
import { Section, Subsection } from '../components/Sections';

export default ({ name }) => (
  <BaseGuide>

{name}

      Routes are simply JavaScript objects with two required props: name and path. There are also a number
      of other props that you can use to enhance the routes. We will cover these below.

    <PrismBlock lang='javascript'>
      {
`{
  name: 'Home',
  path: ''
};`
      }
    </PrismBlock>

        First, we should cover how route matching works. Curi takes an array of route objects. Whenever Curi
        receives a new location, it will walk over the route objects in the order that they are defined in
        the array.

        Sometimes a route's path with only partially match the location's pathname. When this happens, the
        matching behavior will vary based on the route's props. By default, routes perform exact matching.
        This means that when the route only matches part of the pathname, it does not count as a match.

      <PrismBlock lang='javascript'>
        {
`// when the pathname is '/a/Run+The+Jewels+3/Hey+Kids',
// the Album route will partially match the pathname. However,
// Curi looks for complete matches, so it will move on to the
// next route
{
  name: 'Album',
  path: 'a/:album'
}`
        }
      </PrismBlock>

        However, if the route has children, then Curi will check if any of those routes form a complete
        match before moving on to the next route in the routes array.

      <PrismBlock lang='javascript'>
        {
`// when the pathname is '/a/Coloring+Book/All+Night',
// the Album route will partially match the pathname. Then,
// its child route Song will be tested and fully match the pathname.
{
  name: 'Album',
  path: 'a/:album',
  children: [
    {
      name: 'Song',
      path: ':title'
    }
  ]
}`
        }
      </PrismBlock>

        Another possibility happens when you use the <IJS>pathOptions</IJS> object to set{' '}
        <IJS>end: false</IJS>. When you do that, then a route the partially matches will consider
        itself matched.

      <PrismBlock lang='javascript'>
        {
`// when the pathname is '/a/Good+Kid,+M.A.A.D+City/Poetic+Justice',
// the Album route will partially match. However, because it sets
// end to false, the partial match will be used.
{
  name: 'Album',
  path: 'a/:albumID',
  pathOptions: {
    end: false
  }
}`
        }
      </PrismBlock>

        If none of your routes match a pathname, then Curi will set a "404" status on the response object.
        The body property of the response will also be <IJS>undefined</IJS>, so it is important
        that your application checks the response's status when it goes to render a response. You can also
        add a wildcard route (<IJS>path: '*'</IJS>) to the end of your routes array, and that route
        will always match. You may want to still manually set the status to "404" for the wildcard route, but
        it is not required.

      <Subsection
        title='name'
        id='name'
      >

          A unique identifier. This should be a string or a symbol.

      </Subsection>

      <Subsection
        title='path'
        id='path'
      >

          A path-to-regexp style string. This should not have a leading slash. The string
          will be passed to path-to-regexp to generate a regular expression. Any{' '}
          parameters will be identified
          so that they can be parsed out when matching against a location's pathname.

        <Note>
          While path-to-regexp supports arrays and RegExps, only string paths are supported here. This is
          because the path must also be reversible to create a pathname given params.
        </Note>
      </Subsection>

      <Subsection
        title='pathOptions'
        id='pathOptions'
      >

          If you need to provide different path options than{' '}
          the defaults used by path-to-regexp,
          you should specify them with a <IJS>pathOptions</IJS> object.

        <Note>
          If a route has a children array property, it will always have the <IJS>end</IJS>
          {' '} path option set to false.
        </Note>
      </Subsection>

      <Subsection
        title='body'
        id='body'
      >

          The body property gives you the opportunity to set the body property of a response for a given
          route. This must be a function and its return value will be what is set as the response object's
          body property.

        <PrismBlock lang='javascript'>
          {
`// when the user visits /contact, the response object's body
// property will be the Contact value
const contact = {
  name: 'Contact',
  path: 'contact',
  body: () => Contact
};`
          }
        </PrismBlock>

          While the above example returns a single value, you might have multiple page values that you want to
          associate with a route. In that case, you can return an object with a property for each one. Then, when
          you render the application, you would access the values using <IJS>response.body.<property-name></IJS>.
          {' '}If you take this approach, each route's <IJS>body</IJS> function should return an object with the same
          properties.

        <PrismBlock lang='javascript'>
          {
`const routes = [
  {
    name: 'Home',
    path: '',
    body: () => ({ main: Home, nav: HomeNav })
  },
  {
    name: 'Contact',
    path: 'contact',
    body: () => ({ main: Contact, nav: ContactNav })
  }
];

function render(response) {
  const { main, nav } = response.body
  ...
}`
          }
        </PrismBlock>
      </Subsection>

      <Subsection
        title='title'
        id='title'
      >

          You can use the title property of a route to specify a title string that should be set on the response
          when that route matches. This can either be a string or a function. If it is a string, then
          <IJS>response.title</IJS> will be set to the value of <IJS>route.title</IJS>. If
          it is a function, it will be called (and passed the <IJS>response.params</IJS> and{' '}
          <IJS>response.data</IJS> values) to generate the title string.

          If a route does not have a title property, when it matches, the response's title property will be an
          empty string.

        <PrismBlock lang='javascript'>
          {
`// as a string
{
  name: 'Contact',
  path: 'contact',
  title: 'How to contact us'
}

// as a function
{
  name: 'Contact Method',
  path: ':method',
  title: (params, data) => \`Contact via $\{params.method\}\`
}`
          }
        </PrismBlock>
      </Subsection>

      <Subsection
        title='children'
        id='children'
      >

          An optional array of route objects. Any child routes will be matched relative to their parent route's path.
          This means that if a parent route's path string is 'one' and a child route's path string is 'two',
          the child will match when the pathname is 'one/two'.

      </Subsection>

      <Subsection
        title='preload'
        id='preload'
      >

          A function that will only be called the first time that a route matches. This should only be
          used for loading resources that are required for the route to display properly. For example,
          if you are doing code splitting with Webpack using <IJS>import()</IJS>, you would
          load the modules in preload.

          The preload function must return a Promise.

        <PrismBlock lang='javascript'>
          {
`const about = {
  name: 'About',
  path: 'about',
  preload: () => {
    return import('./components/About')
      .then(module => AsyncStore.register(module.default));
  }
};`
          }
        </PrismBlock>
      </Subsection>

      <Subsection
        title='load'
        id='load'
      >

          A function that can be used for data fetching as well as for triggering redirects.
          The load function will be passed the params object that is parsed from the location's
          pathname (using the route and its ancestor's paths), the current location, a modifiers object
          that can be used to modify the response object that will be created, and an object containing
          all of the registered Curi addons (the <IJS>pathname</IJS> addon being particularly useful).

          Like preload, load must return a Promise.

        <PrismBlock lang='javascript'>
          {

`// set response data
const user = {
  name: 'User',
  path: ':id',
  load: (params, location, mod, addons) => {
    return fetch(\`/api/users/$\{params.id\}\`)
      .then(resp => JSON.parse(resp))
      .then(data => mod.setData(data);)
      .catch(err => {
        mod.fail(err);
        mod.setStatus(404);
      });
  }
}

// set a permanent redirect
const routes = [
  {
    name: 'Photo',
    path: 'p/:id'
  },
  {
    name: 'Old Photo',
    path: 'photo/:id',
    load: (params, location, mod, addons) => {
      const pathname = addons.pathname('Photo', params);
      mod.redirect({ ...location, pathname }, 301);
    }
  }
]
// navigating to /photo/123 will automatically redirect to /p/123`
          }
        </PrismBlock>

          What is that modifiers object that gets passed to the load function?
          It contains a number of functions that you can use to modify the response. These
          functions are <IJS>redirect</IJS>, <IJS>fail</IJS>,{' '}
          <IJS>setStatus</IJS>, and <IJS>setData</IJS>.

            <IJS>redirect(to, code)</IJS> - This allows you to turn the response into a
            redirect response. When you application receives a redirect response, it should redirect
            to the new location (using your history object) instead of re-rendering. If you do not
            provide a code, then 301 will be used. Setting the status code is mostly important for
            rendering on the server. The <IJS>to</IJS> argument should be a string or a location
            object. Once the response has been created, Curi will automatically redirect
            to the <IJS>to</IJS> location.

            <IJS>fail(error)</IJS> - A method to call when something goes wrong. This will
            add an error property to the response.

            <IJS>setStatus(code)</IJS> - This method will set a new status for the response
            (the default status is 200 when a route matches and 404 when no routes match).

            <IJS>setData(data)</IJS> - The value passed to this method will be set as the
            response's data property.

      </Subsection>

      <Subsection
        title='extra'
        id='extra'
      >

          If you have any additional properties that you want attached to a route, use the <IJS>extra</IJS> property. You will be
          able to use <IJS>route.extra</IJS> in any custom addons or when a route matches via <IJS>response.route.extra</IJS>.

          You can attach anything you want to <IJS>extra</IJS> or you may never find yourself needing to use this. One possible
          use case for <IJS>extra</IJS> is that you could specify entrance/exit animation types. One route might want to fade in,
          while another might slide in.

        <PrismBlock lang='javascript'>
          {
`const routes = [
  {
    name: 'A Route',
    path: 'a-route',
    extra: {
      transition: 'fade'
    }
  },
  {
    name: 'B Route',
    path: 'b-route',
    extra: {
      enter: 'slide-right'
    }
  }
];`
          }
        </PrismBlock>
      </Subsection>

Next

        Now that you know how to setup your routes, we can get to the good part: actually
        rendering your application using response objects. Check out the{' '}
        <Link to='Guide' params={{ slug: 'responses' }}>Rendering with Responses</Link> guide
        to learn how.

  </BaseGuide>
);
<commit_msg>(docs) route.params [ci skip]<commit_after>import React from 'react';
import BaseGuide from '../components/BaseGuide';
import { InlineJS as IJS, PrismBlock } from '../components/PrismBlocks';
import { Note } from '../components/Messages';
import { Link } from '@curi/react';
import { Section, Subsection } from '../components/Sections';

export default ({ name }) => (
  <BaseGuide>

{name}

      Routes are simply JavaScript objects with two required props: name and path. There are also a number
      of other props that you can use to enhance the routes. We will cover these below.

    <PrismBlock lang='javascript'>
      {
`{
  name: 'Home',
  path: ''
};`
      }
    </PrismBlock>

        First, we should cover how route matching works. Curi takes an array of route objects. Whenever Curi
        receives a new location, it will walk over the route objects in the order that they are defined in
        the array.

        Sometimes a route's path with only partially match the location's pathname. When this happens, the
        matching behavior will vary based on the route's props. By default, routes perform exact matching.
        This means that when the route only matches part of the pathname, it does not count as a match.

      <PrismBlock lang='javascript'>
        {
`// when the pathname is '/a/Run+The+Jewels+3/Hey+Kids',
// the Album route will partially match the pathname. However,
// Curi looks for complete matches, so it will move on to the
// next route
{
  name: 'Album',
  path: 'a/:album'
}`
        }
      </PrismBlock>

        However, if the route has children, then Curi will check if any of those routes form a complete
        match before moving on to the next route in the routes array.

      <PrismBlock lang='javascript'>
        {
`// when the pathname is '/a/Coloring+Book/All+Night',
// the Album route will partially match the pathname. Then,
// its child route Song will be tested and fully match the pathname.
{
  name: 'Album',
  path: 'a/:album',
  children: [
    {
      name: 'Song',
      path: ':title'
    }
  ]
}`
        }
      </PrismBlock>

        Another possibility happens when you use the <IJS>pathOptions</IJS> object to set{' '}
        <IJS>end: false</IJS>. When you do that, then a route the partially matches will consider
        itself matched.

      <PrismBlock lang='javascript'>
        {
`// when the pathname is '/a/Good+Kid,+M.A.A.D+City/Poetic+Justice',
// the Album route will partially match. However, because it sets
// end to false, the partial match will be used.
{
  name: 'Album',
  path: 'a/:albumID',
  pathOptions: {
    end: false
  }
}`
        }
      </PrismBlock>

        If none of your routes match a pathname, then Curi will set a "404" status on the response object.
        The body property of the response will also be <IJS>undefined</IJS>, so it is important
        that your application checks the response's status when it goes to render a response. You can also
        add a wildcard route (<IJS>path: '*'</IJS>) to the end of your routes array, and that route
        will always match. You may want to still manually set the status to "404" for the wildcard route, but
        it is not required.

      <Subsection
        title='name'
        id='name'
      >

          A unique identifier. This should be a string or a symbol.

      </Subsection>

      <Subsection
        title='path'
        id='path'
      >

          A path-to-regexp style string. This should not have a leading slash. The string
          will be passed to path-to-regexp to generate a regular expression. Any{' '}
          parameters will be identified
          so that they can be parsed out when matching against a location's pathname.

        <Note>
          While path-to-regexp supports arrays and RegExps, only string paths are supported here. This is
          because the path must also be reversible to create a pathname given params.
        </Note>
      </Subsection>

      <Subsection
        title='pathOptions'
        id='pathOptions'
      >

          If you need to provide different path options than{' '}
          the defaults used by path-to-regexp,
          you should specify them with a <IJS>pathOptions</IJS> object.

        <Note>
          If a route has a children array property, it will always have the <IJS>end</IJS>
          {' '} path option set to false.
        </Note>
      </Subsection>

      <Subsection
        title='body'
        id='body'
      >

          The body property gives you the opportunity to set the body property of a response for a given
          route. This must be a function and its return value will be what is set as the response object's
          body property.

        <PrismBlock lang='javascript'>
          {
`// when the user visits /contact, the response object's body
// property will be the Contact value
const contact = {
  name: 'Contact',
  path: 'contact',
  body: () => Contact
};`
          }
        </PrismBlock>

          While the above example returns a single value, you might have multiple page values that you want to
          associate with a route. In that case, you can return an object with a property for each one. Then, when
          you render the application, you would access the values using <IJS>response.body.<property-name></IJS>.
          {' '}If you take this approach, each route's <IJS>body</IJS> function should return an object with the same
          properties.

        <PrismBlock lang='javascript'>
          {
`const routes = [
  {
    name: 'Home',
    path: '',
    body: () => ({ main: Home, nav: HomeNav })
  },
  {
    name: 'Contact',
    path: 'contact',
    body: () => ({ main: Contact, nav: ContactNav })
  }
];

function render(response) {
  const { main, nav } = response.body
  ...
}`
          }
        </PrismBlock>
      </Subsection>

      <Subsection
        title='title'
        id='title'
      >

          You can use the title property of a route to specify a title string that should be set on the response
          when that route matches. This can either be a string or a function. If it is a string, then
          <IJS>response.title</IJS> will be set to the value of <IJS>route.title</IJS>. If
          it is a function, it will be called (and passed the <IJS>response.params</IJS> and{' '}
          <IJS>response.data</IJS> values) to generate the title string.

          If a route does not have a title property, when it matches, the response's title property will be an
          empty string.

        <PrismBlock lang='javascript'>
          {
`// as a string
{
  name: 'Contact',
  path: 'contact',
  title: 'How to contact us'
}

// as a function
{
  name: 'Contact Method',
  path: ':method',
  title: (params, data) => \`Contact via $\{params.method\}\`
}`
          }
        </PrismBlock>
      </Subsection>

      <Subsection
        title='children'
        id='children'
      >

          An optional array of route objects. Any child routes will be matched relative to their parent route's path.
          This means that if a parent route's path string is 'one' and a child route's path string is 'two',
          the child will match when the pathname is 'one/two'.

      </Subsection>

      <Subsection
        title='preload'
        id='preload'
      >

          A function that will only be called the first time that a route matches. This should only be
          used for loading resources that are required for the route to display properly. For example,
          if you are doing code splitting with Webpack using <IJS>import()</IJS>, you would
          load the modules in preload.

          The preload function must return a Promise.

        <PrismBlock lang='javascript'>
          {
`const about = {
  name: 'About',
  path: 'about',
  preload: () => {
    return import('./components/About')
      .then(module => AsyncStore.register(module.default));
  }
};`
          }
        </PrismBlock>
      </Subsection>

      <Subsection
        title='load'
        id='load'
      >

          A function that can be used for data fetching as well as for triggering redirects.
          The load function will be passed the params object that is parsed from the location's
          pathname (using the route and its ancestor's paths), the current location, a modifiers object
          that can be used to modify the response object that will be created, and an object containing
          all of the registered Curi addons (the <IJS>pathname</IJS> addon being particularly useful).

          Like preload, load must return a Promise.

        <PrismBlock lang='javascript'>
          {

`// set response data
const user = {
  name: 'User',
  path: ':id',
  load: (params, location, mod, addons) => {
    return fetch(\`/api/users/$\{params.id\}\`)
      .then(resp => JSON.parse(resp))
      .then(data => mod.setData(data);)
      .catch(err => {
        mod.fail(err);
        mod.setStatus(404);
      });
  }
}

// set a permanent redirect
const routes = [
  {
    name: 'Photo',
    path: 'p/:id'
  },
  {
    name: 'Old Photo',
    path: 'photo/:id',
    load: (params, location, mod, addons) => {
      const pathname = addons.pathname('Photo', params);
      mod.redirect({ ...location, pathname }, 301);
    }
  }
]
// navigating to /photo/123 will automatically redirect to /p/123`
          }
        </PrismBlock>

          What is that modifiers object that gets passed to the load function?
          It contains a number of functions that you can use to modify the response. These
          functions are <IJS>redirect</IJS>, <IJS>fail</IJS>,{' '}
          <IJS>setStatus</IJS>, and <IJS>setData</IJS>.

            <IJS>redirect(to, code)</IJS> - This allows you to turn the response into a
            redirect response. When you application receives a redirect response, it should redirect
            to the new location (using your history object) instead of re-rendering. If you do not
            provide a code, then 301 will be used. Setting the status code is mostly important for
            rendering on the server. The <IJS>to</IJS> argument should be a string or a location
            object. Once the response has been created, Curi will automatically redirect
            to the <IJS>to</IJS> location.

            <IJS>fail(error)</IJS> - A method to call when something goes wrong. This will
            add an error property to the response.

            <IJS>setStatus(code)</IJS> - This method will set a new status for the response
            (the default status is 200 when a route matches and 404 when no routes match).

            <IJS>setData(data)</IJS> - The value passed to this method will be set as the
            response's data property.

      </Subsection>

      <Subsection
        title='params'
        id='params'
      >

          When <IJS>path-to-regexp</IJS> matches your paths, all parameters are extracted as strings. However, you might have
          some route params to be other types. You can provide functions to transform params using the <IJS>route.params</IJS>
          {' '}object. To transform a param, its name should be the string value from the path. The paired value should be a
          function that takes a string (the value from the pathname) and returns a new value (transformed however you want).

        <PrismBlock lang='javascript'>
          {
`const routes = [
  {
    name: 'Number',
    path: 'number/:num',
    params: {
      num: n => parseInt(n, 10)
    }
  }
]
// when the user visits /number/1,
// response.params will be { num: 1 } instead of { num: "1" }`
          }
        </PrismBlock>
      </Subsection>

      <Subsection
        title='extra'
        id='extra'
      >

          If you have any additional properties that you want attached to a route, use the <IJS>extra</IJS> property. You will be
          able to use <IJS>route.extra</IJS> in any custom addons or when a route matches via <IJS>response.route.extra</IJS>.

          You can attach anything you want to <IJS>extra</IJS> or you may never find yourself needing to use this. One possible
          use case for <IJS>extra</IJS> is that you could specify entrance/exit animation types. One route might want to fade in,
          while another might slide in.

        <PrismBlock lang='javascript'>
          {
`const routes = [
  {
    name: 'A Route',
    path: 'a-route',
    extra: {
      transition: 'fade'
    }
  },
  {
    name: 'B Route',
    path: 'b-route',
    extra: {
      enter: 'slide-right'
    }
  }
];`
          }
        </PrismBlock>
      </Subsection>

Next

        Now that you know how to setup your routes, we can get to the good part: actually
        rendering your application using response objects. Check out the{' '}
        <Link to='Guide' params={{ slug: 'responses' }}>Rendering with Responses</Link> guide
        to learn how.

  </BaseGuide>
);
<|endoftext|>


<commit_before>'use strict';

var Helper = require('../../helper'),
    XMLHelper = require('../../xml-helper');

var toXML = XMLHelper.toXML,
    validate = XMLHelper.validate;

var camundaPackage = require('../../fixtures/json/model/camunda');

describe('bpmn-moddle - integration', function() {

  describe('camunda extension', function() {

    var moddle = Helper.createModdle({ camunda: camundaPackage });

    function fromFile(file, done) {
      XMLHelper.fromFile(moddle, file, done);
    }

    describe('should serialize valid BPMN 2.0 after read', function() {

      it('inputOutput', function(done) {

        // given
        fromFile('test/fixtures/bpmn/extension/camunda/inputOutput.bpmn', function(err, result) {

          if (err) {
            return done(err);
          }

          // when
          toXML(result, { format: true }, function(err, xml) {
            validate(err, xml, done);
          });
        });
      });

    });

  });

});<commit_msg>chore(tests): increase test timeout<commit_after>'use strict';

var Helper = require('../../helper'),
    XMLHelper = require('../../xml-helper');

var toXML = XMLHelper.toXML,
    validate = XMLHelper.validate;

var camundaPackage = require('../../fixtures/json/model/camunda');

describe('bpmn-moddle - integration', function() {

  describe('camunda extension', function() {

    var moddle = Helper.createModdle({ camunda: camundaPackage });

    function fromFile(file, done) {
      XMLHelper.fromFile(moddle, file, done);
    }

    describe('should serialize valid BPMN 2.0 after read', function() {

      this.timeout(15000);

      it('inputOutput', function(done) {

        // given
        fromFile('test/fixtures/bpmn/extension/camunda/inputOutput.bpmn', function(err, result) {

          if (err) {
            return done(err);
          }

          // when
          toXML(result, { format: true }, function(err, xml) {
            validate(err, xml, done);
          });
        });
      });

    });

  });

});<|endoftext|>


<commit_before>import axios from 'axios';
import React, {Component} from 'react';

export class Quotes extends Component {
    constructor(props) {
        super(props);
        this.state = {
            quoteText: ''
        }
    } /*Here we set our state which will be updated next when the component loads or mounts*/

    componentDidMount() {
        axios.get('https://api.kanye.rest')
            .then(response => {
                this.setState({ quoteText: response.data["quote"] });
            })
            .catch(error => {
                console.log(error);
            });
    }        /*Here we call our 3rd party api for quotes, their formatting needs to be understood, can use postman to understand and
    manage. The returned quote then becomes the current state. We also use a callback to load any errors in the console. */

render() {
    const { quoteText } = this.state;
    return (

"{quoteText}"

            <button onClick={this.getClicked}
                    type='button'>More Wisdom</button>

    );
}   /*Here we actually show the result in jsx form as a variable and finally export it as a component to the parent
where it can be displayed*/
}

export default Quotes;

<commit_msg>Added button<commit_after>import axios from 'axios';
import React, {Component} from 'react';

export class Quotes extends Component {
    constructor(props) {
        super(props);
        this.state = {
            quoteText: ''
        }
        this.handleClick = this.handleClick.bind(this)
    } /*Here we set our state which will be updated next when the component loads or mounts*/

    handleClick () {
        axios.get('https://api.kanye.rest')
            .then(response => this.setState({ quoteText: response.data["quote"] }))
    }

    componentDidMount() {
        axios.get('https://api.kanye.rest')
            .then(response => {
                this.setState({ quoteText: response.data["quote"] });
            })
            .catch(error => {
                console.log(error);
            });
    }        /*Here we call our 3rd party api for quotes, their formatting needs to be understood, can use postman to understand and
    manage. The returned quote then becomes the current state. We also use a callback to load any errors in the console. */

render() {
    const { quoteText } = this.state;
    return (

"{quoteText}"

            <button className="button" onClick={this.handleClick}>More Wisdom</button>

    );
}   /*Here we actually show the result in jsx form as a variable and finally export it as a component to the parent
where it can be displayed*/
}

export default Quotes;

<|endoftext|>


<commit_before>#!/usr/bin/env node

var express = require('express');
var gss = require('gss');
var http = require('http');
var path = require('path');
var util = require('util');

var auth = require('../lib/auth.js');
var conf = require('../lib/config.js');
var connections = require('../lib/connections.js');
var db = require('../lib/db.js');
var error = require('../lib/error.js');
var msgid = require('../lib/msgid.js');
var Subscriber = require('../lib/subscriber.js').Subscriber;

// Set the keytab.
var realAuth = false;
if (conf.get('serverKeytab')) {
  process.env['KRB5_KTNAME'] = conf.get('serverKeytab');
  realAuth = true;
} else {
  console.error('!!!!!!!!!!!!!!!!!!!!!');
  console.error('No keytab set. Using fake authentication');
  console.error('Do NOT run this in production!');
  console.error('!!!!!!!!!!!!!!!!!!!!!');
}

function sendError(res, err) {
  if (err instanceof error.UserError) {
    // Blegh.
    if (err.code == 401)
      res.set('WWW-Authenticate', 'Bearer');
    res.send(err.code, err.msg);
  } else {
    res.send(500);
  }
}

var subscriber = new Subscriber();

var app = express();

app.use(express.bodyParser());
// CORS ALL THE THINGS. We won't use cookies and this is different
// from Access-Control-Allow-Credentials. So we're fine.
app.use(function(req, res, next) {
  res.set('Access-Control-Allow-Origin', '*');
  next();
});

function requireUser(req, res, next) {
  // IE9 CORS (really XDomainRequest) doesn't allow custom headers.
  // And even with CORS, an Authorization header would require a
  // preflight. Allow passing the token in the query string. Google
  // does this for their APIs.
  //
  // TODO(davidben): Allow passing it into the Authorization header
  // anyway. Non-browser clients might appreciate a more HTTP-like
  // header. Google uses
  //
  //   Authorization: Bearer ${token}
  //
  // which seems sane enough. But to be Proper, I ought to look up
  // exactly how to parse those.
  //
  // See http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html
  var token = req.query.access_token;
  if (!token) {
    // Appease the HTTP gods who say you need a WWW-Authenticate
    // header when you send back 401. Hopefully this'll prevent a
    // browser prompt. They're bad about not prompting...
    res.set('WWW-Authenticate', 'Bearer');
    res.send(401, 'Auth token required');
    return;
  }
  auth.checkAuthToken(token).then(function(user) {
    req.user = user;
    next();
  }, function(err) {
    sendError(res, err);
    console.error(err);
  });
}

app.post('/api/v1/auth', function(req, res) {
  var principal, respTokenB64;
  if (realAuth) {
    if (typeof req.body.token !== 'string') {
      res.send(400, 'Token expected');
      return;
    }

    var context = gss.createAcceptor(null);
    try {
      var respToken =
        context.acceptSecContext(new Buffer(req.body.token, 'base64'));
    } catch (e) {
      res.send(403, 'Bad token');
      console.error(e);
      return;
    }

    if (!context.isEstablished()) {
      // We don't support multi-legged auth. But this shouldn't
      // happen.
      res.send(500, 'Internal error');
      console.error('ERROR: GSS context did not establish in iteration!');
      return;
    }

    principal = context.srcName().toString();
    if (respToken != null)
      respTokenB64 = respToken.toString('base64');
  } else {
    principal = req.body.principal;
    if (typeof principal !== "string") {
      res.send(400, 'Principal expected');
      return;
    }
  }

  var userPromise;
  if (req.body.createUser) {
    // Create user and make default subs.
    userPromise = db.getOrCreateUser(principal).then(function(user) {
      if (!user.newUser)
        return user;
      return subscriber.addDefaultUserSubscriptions(user).then(function() {
        return user;
      });
    });
  } else {
    userPromise = db.getUser(principal);
  }

  userPromise.then(function(user) {
    if (user == null)
      throw new error.UserError(403, 'User does not exist');
    return user;
  }).then(function(user) {
    var token = auth.makeAuthToken(user);
    res.json(200, {
      gssToken: respTokenB64,
      authToken: token
    });
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/ping', requireUser, function(req, res) {
  res.json(200, { pong: 1 });
});

app.get('/api/v1/info', requireUser, function(req, res) {
  db.getUserInfo(req.user).then(function(ret) {
    res.json(200, ret);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.post('/api/v1/info', requireUser, function(req, res) {
  if (typeof req.body.info != 'string' ||
      typeof req.body.expectedVersion != 'number') {
    res.send(400, 'info and expectedVersion required');
    return;
  }
  db.updateUserInfo(
    req.user, req.body.info, req.body.expectedVersion
  ).then(function(updated) {
    if (updated)
      return { updated: true };
    return db.getUserInfo(req.user).then(function(ret) {
      return {
        updated: false,
        version: ret.version,
        info: ret.info
      };
    });
  }).then(function(ret) {
    res.json(200, ret);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/subscriptions', requireUser, function(req, res) {
  db.getUserSubscriptions(req.user).then(function(subs) {
    res.json(200, subs);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

function isValidSub(sub) {
  if (typeof sub !== 'object')
    return false;
  if (typeof sub.class !== 'string')
    return false;
  if (sub.instance !== null && typeof sub.instance !== 'string')
    return false;
  if (typeof sub.recipient !== 'string')
    return false;
  return true;
}

app.post('/api/v1/subscribe', requireUser, function(req, res) {
  if (!isValidSub(req.body.subscription)) {
    // TODO(davidben): Nicer error message.
    res.send(400, 'Subscription triple expected');
    return;
  }
  subscriber.addUserSubscription(
    req.user, req.body.subscription, req.body.credentials
  ).then(function(sub) {
    res.json(200, sub);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.post('/api/v1/unsubscribe', requireUser, function(req, res) {
  if (!isValidSub(req.body.subscription)) {
    // TODO(davidben): Nicer error message.
    res.send(400, 'Subscription triple expected');
    return;
  }
  subscriber.removeUserSubscription(
    req.user,
    req.body.subscription
  ).then(function() {
    res.json(200, { unsubscribed: true });
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/messages', requireUser, function(req, res) {
  var offset = req.query.offset;
  if (offset) {
    offset = msgid.unseal(offset);
  } else {
    // Punt the empty string too.
    offset = null;
  }
  db.getMessages(
    req.user, offset, {
      inclusive: Boolean(req.query.inclusive|0),
      reverse: Boolean(req.query.reverse|0),
      limit: req.query.count|0
    }
  ).then(function(result) {
    result.messages.forEach(function(msg) {
      msg.id = msgid.seal(msg.id);
    });
    res.json(200, result);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/zephyrcreds', requireUser, function(req, res) {
  res.json(200, {
    needsRefresh: subscriber.needsZephyrCreds(req.user)
  });
});

app.post('/api/v1/zephyrcreds', requireUser, function(req, res) {
  if (!req.body.credentials) {
    res.send(400, "Missing credentials parameter");
  }
  subscriber.refreshPrivateSubs(
    req.user, req.body.credentials
  ).then(function() {
    res.json(200, { refreshed: true });
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.use(express.static(path.join(__dirname, '../static')));

var server = http.createServer(app);
var connectionManager = connections.listen(server, subscriber);

// Load active subscriptions from the database.
console.log('Starting subscriber...');
subscriber.start().then(function() {
  // And now we're ready to start doing things.
  console.log('...started');
  server.listen(conf.get('port'), conf.get('ip'), function() {
    var addy = server.address();
    console.log('running on http://' + addy.address + ":" + addy.port);
  });
}).done();

// Cancel subscriptions on exit.
['SIGINT', 'SIGQUIT', 'SIGTERM'].forEach(function(sig) {
  process.on(sig, function() {
    console.log('Canceling subscriptions...');
    subscriber.shutdown().then(function() {
      console.log('Bye');
      process.exit();
    }).done();
  });
});
<commit_msg>Allow Content-Type via CORS<commit_after>#!/usr/bin/env node

var express = require('express');
var gss = require('gss');
var http = require('http');
var path = require('path');
var util = require('util');

var auth = require('../lib/auth.js');
var conf = require('../lib/config.js');
var connections = require('../lib/connections.js');
var db = require('../lib/db.js');
var error = require('../lib/error.js');
var msgid = require('../lib/msgid.js');
var Subscriber = require('../lib/subscriber.js').Subscriber;

// Set the keytab.
var realAuth = false;
if (conf.get('serverKeytab')) {
  process.env['KRB5_KTNAME'] = conf.get('serverKeytab');
  realAuth = true;
} else {
  console.error('!!!!!!!!!!!!!!!!!!!!!');
  console.error('No keytab set. Using fake authentication');
  console.error('Do NOT run this in production!');
  console.error('!!!!!!!!!!!!!!!!!!!!!');
}

function sendError(res, err) {
  if (err instanceof error.UserError) {
    // Blegh.
    if (err.code == 401)
      res.set('WWW-Authenticate', 'Bearer');
    res.send(err.code, err.msg);
  } else {
    res.send(500);
  }
}

var subscriber = new Subscriber();

var app = express();

app.use(express.bodyParser());
// CORS ALL THE THINGS. We won't use cookies and this is different
// from Access-Control-Allow-Credentials. So we're fine.
app.use(function(req, res, next) {
  res.set('Access-Control-Allow-Origin', '*');
  // TODO(davidben): Unfortunately, using JSON as the Content-Type
  // doesn't save us from preflights.
  res.set('Access-Control-Allow-Headers', 'Content-Type');
  next();
});
// Just accept the preflight everywhere. Whatever.
app.options('*', function(req, res) {
  // Pick up the headers from earlier.
  res.send(200);
});

function requireUser(req, res, next) {
  // IE9 CORS (really XDomainRequest) doesn't allow custom headers.
  // And even with CORS, an Authorization header would require a
  // preflight. Allow passing the token in the query string. Google
  // does this for their APIs.
  //
  // TODO(davidben): Allow passing it into the Authorization header
  // anyway. Non-browser clients might appreciate a more HTTP-like
  // header. Google uses
  //
  //   Authorization: Bearer ${token}
  //
  // which seems sane enough. But to be Proper, I ought to look up
  // exactly how to parse those.
  //
  // See http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html
  var token = req.query.access_token;
  if (!token) {
    // Appease the HTTP gods who say you need a WWW-Authenticate
    // header when you send back 401. Hopefully this'll prevent a
    // browser prompt. They're bad about not prompting...
    res.set('WWW-Authenticate', 'Bearer');
    res.send(401, 'Auth token required');
    return;
  }
  auth.checkAuthToken(token).then(function(user) {
    req.user = user;
    next();
  }, function(err) {
    sendError(res, err);
    console.error(err);
  });
}

app.post('/api/v1/auth', function(req, res) {
  var principal, respTokenB64;
  if (realAuth) {
    if (typeof req.body.token !== 'string') {
      res.send(400, 'Token expected');
      return;
    }

    var context = gss.createAcceptor(null);
    try {
      var respToken =
        context.acceptSecContext(new Buffer(req.body.token, 'base64'));
    } catch (e) {
      res.send(403, 'Bad token');
      console.error(e);
      return;
    }

    if (!context.isEstablished()) {
      // We don't support multi-legged auth. But this shouldn't
      // happen.
      res.send(500, 'Internal error');
      console.error('ERROR: GSS context did not establish in iteration!');
      return;
    }

    principal = context.srcName().toString();
    if (respToken != null)
      respTokenB64 = respToken.toString('base64');
  } else {
    principal = req.body.principal;
    if (typeof principal !== "string") {
      res.send(400, 'Principal expected');
      return;
    }
  }

  var userPromise;
  if (req.body.createUser) {
    // Create user and make default subs.
    userPromise = db.getOrCreateUser(principal).then(function(user) {
      if (!user.newUser)
        return user;
      return subscriber.addDefaultUserSubscriptions(user).then(function() {
        return user;
      });
    });
  } else {
    userPromise = db.getUser(principal);
  }

  userPromise.then(function(user) {
    if (user == null)
      throw new error.UserError(403, 'User does not exist');
    return user;
  }).then(function(user) {
    var token = auth.makeAuthToken(user);
    res.json(200, {
      gssToken: respTokenB64,
      authToken: token
    });
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/ping', requireUser, function(req, res) {
  res.json(200, { pong: 1 });
});

app.get('/api/v1/info', requireUser, function(req, res) {
  db.getUserInfo(req.user).then(function(ret) {
    res.json(200, ret);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.post('/api/v1/info', requireUser, function(req, res) {
  if (typeof req.body.info != 'string' ||
      typeof req.body.expectedVersion != 'number') {
    res.send(400, 'info and expectedVersion required');
    return;
  }
  db.updateUserInfo(
    req.user, req.body.info, req.body.expectedVersion
  ).then(function(updated) {
    if (updated)
      return { updated: true };
    return db.getUserInfo(req.user).then(function(ret) {
      return {
        updated: false,
        version: ret.version,
        info: ret.info
      };
    });
  }).then(function(ret) {
    res.json(200, ret);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/subscriptions', requireUser, function(req, res) {
  db.getUserSubscriptions(req.user).then(function(subs) {
    res.json(200, subs);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

function isValidSub(sub) {
  if (typeof sub !== 'object')
    return false;
  if (typeof sub.class !== 'string')
    return false;
  if (sub.instance !== null && typeof sub.instance !== 'string')
    return false;
  if (typeof sub.recipient !== 'string')
    return false;
  return true;
}

app.post('/api/v1/subscribe', requireUser, function(req, res) {
  if (!isValidSub(req.body.subscription)) {
    // TODO(davidben): Nicer error message.
    res.send(400, 'Subscription triple expected');
    return;
  }
  subscriber.addUserSubscription(
    req.user, req.body.subscription, req.body.credentials
  ).then(function(sub) {
    res.json(200, sub);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.post('/api/v1/unsubscribe', requireUser, function(req, res) {
  if (!isValidSub(req.body.subscription)) {
    // TODO(davidben): Nicer error message.
    res.send(400, 'Subscription triple expected');
    return;
  }
  subscriber.removeUserSubscription(
    req.user,
    req.body.subscription
  ).then(function() {
    res.json(200, { unsubscribed: true });
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/messages', requireUser, function(req, res) {
  var offset = req.query.offset;
  if (offset) {
    offset = msgid.unseal(offset);
  } else {
    // Punt the empty string too.
    offset = null;
  }
  db.getMessages(
    req.user, offset, {
      inclusive: Boolean(req.query.inclusive|0),
      reverse: Boolean(req.query.reverse|0),
      limit: req.query.count|0
    }
  ).then(function(result) {
    result.messages.forEach(function(msg) {
      msg.id = msgid.seal(msg.id);
    });
    res.json(200, result);
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.get('/api/v1/zephyrcreds', requireUser, function(req, res) {
  res.json(200, {
    needsRefresh: subscriber.needsZephyrCreds(req.user)
  });
});

app.post('/api/v1/zephyrcreds', requireUser, function(req, res) {
  if (!req.body.credentials) {
    res.send(400, "Missing credentials parameter");
  }
  subscriber.refreshPrivateSubs(
    req.user, req.body.credentials
  ).then(function() {
    res.json(200, { refreshed: true });
  }, function(err) {
    sendError(res, err);
    console.error(err);
  }).done();
});

app.use(express.static(path.join(__dirname, '../static')));

var server = http.createServer(app);
var connectionManager = connections.listen(server, subscriber);

// Load active subscriptions from the database.
console.log('Starting subscriber...');
subscriber.start().then(function() {
  // And now we're ready to start doing things.
  console.log('...started');
  server.listen(conf.get('port'), conf.get('ip'), function() {
    var addy = server.address();
    console.log('running on http://' + addy.address + ":" + addy.port);
  });
}).done();

// Cancel subscriptions on exit.
['SIGINT', 'SIGQUIT', 'SIGTERM'].forEach(function(sig) {
  process.on(sig, function() {
    console.log('Canceling subscriptions...');
    subscriber.shutdown().then(function() {
      console.log('Bye');
      process.exit();
    }).done();
  });
});
<|endoftext|>