<commit_before>/**
 * @license
 * Copyright 2018 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Integrated Development Environment for Code City.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

Code.ObjectPanel = {};

/**
 * List of parts (passed in by the URL hash).
 * @type {?Array<!Object>}
 */
Code.ObjectPanel.parts = null;

/**
 * DOM node containing the list of properties.
 * @type {?Element}
 */
Code.ObjectPanel.tableBody = null;

/**
 * Structured data from Code City.
 * @type {?Object}
 */
Code.ObjectPanel.data = null;

/**
 * Page has loaded, initialize the panel.  Called by Code City with data.
 */
Code.ObjectPanel.init = function() {
  Code.ObjectPanel.tableBody = document.getElementById('objectTableBody');
  // Clear the '...'
  Code.ObjectPanel.tableBody.innerHTML = '';
  var data = Code.ObjectPanel.data;
  if (!data) {
    // Server error.  Should not happen.
    var title = document.getElementById('objectTitle');
    title.className = 'objectFailTitle';
    var fail = document.getElementById('objectFail');
    fail.style.display = 'table-row';
    return;
  }
  if (data.roots) {
    // Print all root objects.
    for (var root of data.roots) {
      var part = {type: 'id', value: root.name};
      Code.ObjectPanel.addLink(part, root.type, false);
    }
  }
  if (data.properties) {
    Code.ObjectPanel.filterShadowed(data.properties);
    // Print all properties of this object.
    for (var i = 0; i < data.properties.length; i++) {
      var propList = data.properties[i];
      for (var j = 0; j < propList.length; j++) {
        var part = {type: 'id', value: propList[j].name};
        Code.ObjectPanel.addLink(part, propList[j].type, i && !j);
      }
    }
    var part = {type: '^'};
    Code.ObjectPanel.addLink(part, 'object', true);
  }

  // Position the type symbols, and monitor for layout changes.
  Code.ObjectPanel.positionTypes();
  window.addEventListener('scroll', Code.ObjectPanel.positionTypes, false);
  window.addEventListener('resize', Code.ObjectPanel.positionTypes, false);

  // Highlight current item, and monitor for path changes.
  Code.ObjectPanel.highlight();
  window.addEventListener('message', Code.ObjectPanel.highlight, false);
};

/**
 * Create the DOM elements to add one property link to the list.
 * @param {!Object} part Single selector part.
 * @param {string} type Type of the property value.
 * @param {boolean} section Flag indicating a new section.
 */
Code.ObjectPanel.addLink = function(part, type, section) {
  var newParts = Code.ObjectPanel.parts.concat(part);
  var selector = Code.Common.partsToSelector(newParts);
  var a = document.createElement('a');
  a.href = '/code?' + encodeURIComponent(selector);
  a.target = '_blank';
  a.setAttribute('data-link', JSON.stringify(part));
  a.addEventListener('click', Code.ObjectPanel.click);
  var text = document.createTextNode(Code.Common.partsToSelector([part]));
  a.appendChild(text);
  var td = document.createElement('td');
  if (section) {
    td.className = 'section';
  }
  var typeSymbol = Code.ObjectPanel.TYPES[type];
  if (typeSymbol) {
    var div = document.createElement('div');
    div.className = 'objectType';
    div.appendChild(document.createTextNode(typeSymbol));
    div.title = type;
    td.appendChild(div);
  }
  td.appendChild(a);
  var tr = document.createElement('tr');
  tr.appendChild(td);
  Code.ObjectPanel.tableBody.appendChild(tr);
};

/**
 * Symbols to print next to properties.
 */
Code.ObjectPanel.TYPES = {
  'object': '{}',
  'symbol': '☆',
  'function': '𝑓'
};

/**
 * When scrollbar is moved or size changes reposition the floating types.
 */
Code.ObjectPanel.positionTypes = function() {
  var left = (document.body.clientWidth + window.scrollX - 18) + 'px';
  var types = document.getElementsByClassName('objectType');
  for (var t of types) {
    t.style.left = left;
  }
};

/**
 * When a property is clicked, trigger an update.
 * @param {!Event} e Click event.
 */
Code.ObjectPanel.click = function(e) {
  if (e.metaKey || e.ctrlKey) {
    return;
  }
  var part = JSON.parse(e.currentTarget.getAttribute('data-link'));
  var newParts = Code.ObjectPanel.parts.concat(part);
  var selector = Code.Common.partsToSelector(newParts);
  // Store the new selector in sessionStorage for all frames to see.
  sessionStorage.setItem(Code.Common.SELECTOR, selector);
  // Alert the top "/code" frameset that there's been a selector change.
  window.parent.parent.postMessage('ping', '*');
  // Don't navigate to this link.
  e.preventDefault();
};

/**
 * Currently selected property, or null if none.
 * @type {?Element}
 */
Code.ObjectPanel.highlighted = null;

/**
 * Highlight the currently selected property (if any).
 * The current selection is based on a value in sessionStorage.
 */
Code.ObjectPanel.highlight = function() {
  var selector = sessionStorage.getItem(Code.Common.SELECTOR);
  var parts = Code.Common.selectorToParts(selector);
  var part = parts ? parts[Code.ObjectPanel.parts.length] : null;
  var jsonPart = JSON.stringify(part);
  var newHighlighted = null;
  for (var tr of Code.ObjectPanel.tableBody.childNodes) {
    var td = tr.firstChild;
    var link = td.lastChild;  // There might be an objectType div first.
    if (link.getAttribute('data-link') === jsonPart) {
      newHighlighted = td;
    }
  }
  if (newHighlighted !== Code.ObjectPanel.highlighted) {
    if (Code.ObjectPanel.highlighted) {
      Code.ObjectPanel.highlighted.className = '';
    }
    if (newHighlighted) {
      newHighlighted.className = 'highlighted';
      if (newHighlighted.scrollIntoView) {
        newHighlighted.scrollIntoView({block: 'nearest', inline: 'nearest'});
      }
    }
    Code.ObjectPanel.highlighted = newHighlighted;
  }
};

/**
 * Remove any properties that are shadowed by objects higher on the inheritance
 * chain.  Also sort the properties alphabetically.
 * @param {Array<!Array<!Object>>} data Property names from Code City.
 */
Code.ObjectPanel.filterShadowed = function(data) {
  if (!data || data.length < 2) {
    return;
  }
  var seen = Object.create(null);
  for (var datum of data) {
    var cursorInsert = 0;
    var cursorRead = 0;
    while (cursorRead < datum.length) {
      var prop = datum[cursorRead++];
      if (!seen[prop.name]) {
        seen[prop.name] = true;
        datum[cursorInsert++] = prop;
      }
    }
    datum.length = cursorInsert;
    datum.sort(Code.ObjectPanel.caseInsensitiveComp);
  }
};

/**
 * Comparison function to sort named objects A-Z without regard to case.
 * @param {string} a One string.
 * @param {string} b Another string.
 * @return {number} -1/0/1 comparator value.
 */
Code.ObjectPanel.caseInsensitiveComp = function(a, b) {
  return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
};

if (!window.TEST) {
  (function() {
    // Load the data from Code City.
    var hash = location.hash.substring(1);
    var script = document.createElement('script');
    script.src = '/code/objectPanel?parts=' + hash;
    document.head.appendChild(script);

    // Fill in the object name.
    Code.ObjectPanel.parts = JSON.parse(decodeURIComponent(hash));
    var div = document.getElementById('objectTitle');
    var lastPart = Code.ObjectPanel.parts[Code.ObjectPanel.parts.length - 1];
    var name;
    if (!lastPart) {
      name = 'Globals';
    } else if (Code.ObjectPanel.parts.length === 1) {
      // Render as 'foo' or '[42]' or '["???"]' or '^'.
      name = Code.Common.partsToSelector([lastPart]);
    } else {
      // Render as '.foo' or '[42]' or '["???"]' or '^'.
      var mockParts = [{type: 'id', value: 'X'}, lastPart];
      name = Code.Common.partsToSelector(mockParts).substring(1);
    }
    div.innerHTML = '';
    div.appendChild(document.createTextNode(name));
  })();

  window.addEventListener('load', Code.ObjectPanel.init);
}
<commit_msg>Add more type symbols<commit_after>/**
 * @license
 * Copyright 2018 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Integrated Development Environment for Code City.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

Code.ObjectPanel = {};

/**
 * List of parts (passed in by the URL hash).
 * @type {?Array<!Object>}
 */
Code.ObjectPanel.parts = null;

/**
 * DOM node containing the list of properties.
 * @type {?Element}
 */
Code.ObjectPanel.tableBody = null;

/**
 * Structured data from Code City.
 * @type {?Object}
 */
Code.ObjectPanel.data = null;

/**
 * Page has loaded, initialize the panel.  Called by Code City with data.
 */
Code.ObjectPanel.init = function() {
  Code.ObjectPanel.tableBody = document.getElementById('objectTableBody');
  // Clear the '...'
  Code.ObjectPanel.tableBody.innerHTML = '';
  var data = Code.ObjectPanel.data;
  if (!data) {
    // Server error.  Should not happen.
    var title = document.getElementById('objectTitle');
    title.className = 'objectFailTitle';
    var fail = document.getElementById('objectFail');
    fail.style.display = 'table-row';
    return;
  }
  if (data.roots) {
    // Print all root objects.
    for (var root of data.roots) {
      var part = {type: 'id', value: root.name};
      Code.ObjectPanel.addLink(part, root.type, false);
    }
  }
  if (data.properties) {
    Code.ObjectPanel.filterShadowed(data.properties);
    // Print all properties of this object.
    for (var i = 0; i < data.properties.length; i++) {
      var propList = data.properties[i];
      for (var j = 0; j < propList.length; j++) {
        var part = {type: 'id', value: propList[j].name};
        Code.ObjectPanel.addLink(part, propList[j].type, i && !j);
      }
    }
    var part = {type: '^'};
    Code.ObjectPanel.addLink(part, 'object', true);
  }

  // Position the type symbols, and monitor for layout changes.
  Code.ObjectPanel.positionTypes();
  window.addEventListener('scroll', Code.ObjectPanel.positionTypes, false);
  window.addEventListener('resize', Code.ObjectPanel.positionTypes, false);

  // Highlight current item, and monitor for path changes.
  Code.ObjectPanel.highlight();
  window.addEventListener('message', Code.ObjectPanel.highlight, false);
};

/**
 * Create the DOM elements to add one property link to the list.
 * @param {!Object} part Single selector part.
 * @param {string} type Type of the property value.
 * @param {boolean} section Flag indicating a new section.
 */
Code.ObjectPanel.addLink = function(part, type, section) {
  var newParts = Code.ObjectPanel.parts.concat(part);
  var selector = Code.Common.partsToSelector(newParts);
  var a = document.createElement('a');
  a.href = '/code?' + encodeURIComponent(selector);
  a.target = '_blank';
  a.setAttribute('data-link', JSON.stringify(part));
  a.addEventListener('click', Code.ObjectPanel.click);
  var text = document.createTextNode(Code.Common.partsToSelector([part]));
  a.appendChild(text);
  var td = document.createElement('td');
  if (section) {
    td.className = 'section';
  }
  var typeSymbol = Code.ObjectPanel.TYPES[type];
  if (typeSymbol) {
    var div = document.createElement('div');
    div.className = 'objectType';
    div.appendChild(document.createTextNode(typeSymbol));
    div.title = type;
    td.appendChild(div);
  }
  td.appendChild(a);
  var tr = document.createElement('tr');
  tr.appendChild(td);
  Code.ObjectPanel.tableBody.appendChild(tr);
};

/**
 * Symbols to print next to properties.
 */
Code.ObjectPanel.TYPES = {
  'array': '[]',
  'boolean': '⏼',
  'function': '𝑓',
  'number': '#',
  'object': '{}',
  'string': '”',
  'symbol': '☆'
};

/**
 * When scrollbar is moved or size changes reposition the floating types.
 */
Code.ObjectPanel.positionTypes = function() {
  var left = (document.body.clientWidth + window.scrollX - 18) + 'px';
  var types = document.getElementsByClassName('objectType');
  for (var t of types) {
    t.style.left = left;
  }
};

/**
 * When a property is clicked, trigger an update.
 * @param {!Event} e Click event.
 */
Code.ObjectPanel.click = function(e) {
  if (e.metaKey || e.ctrlKey) {
    return;
  }
  var part = JSON.parse(e.currentTarget.getAttribute('data-link'));
  var newParts = Code.ObjectPanel.parts.concat(part);
  var selector = Code.Common.partsToSelector(newParts);
  // Store the new selector in sessionStorage for all frames to see.
  sessionStorage.setItem(Code.Common.SELECTOR, selector);
  // Alert the top "/code" frameset that there's been a selector change.
  window.parent.parent.postMessage('ping', '*');
  // Don't navigate to this link.
  e.preventDefault();
};

/**
 * Currently selected property, or null if none.
 * @type {?Element}
 */
Code.ObjectPanel.highlighted = null;

/**
 * Highlight the currently selected property (if any).
 * The current selection is based on a value in sessionStorage.
 */
Code.ObjectPanel.highlight = function() {
  var selector = sessionStorage.getItem(Code.Common.SELECTOR);
  var parts = Code.Common.selectorToParts(selector);
  var part = parts ? parts[Code.ObjectPanel.parts.length] : null;
  var jsonPart = JSON.stringify(part);
  var newHighlighted = null;
  for (var tr of Code.ObjectPanel.tableBody.childNodes) {
    var td = tr.firstChild;
    var link = td.lastChild;  // There might be an objectType div first.
    if (link.getAttribute('data-link') === jsonPart) {
      newHighlighted = td;
    }
  }
  if (newHighlighted !== Code.ObjectPanel.highlighted) {
    if (Code.ObjectPanel.highlighted) {
      Code.ObjectPanel.highlighted.className = '';
    }
    if (newHighlighted) {
      newHighlighted.className = 'highlighted';
      if (newHighlighted.scrollIntoView) {
        newHighlighted.scrollIntoView({block: 'nearest', inline: 'nearest'});
      }
    }
    Code.ObjectPanel.highlighted = newHighlighted;
  }
};

/**
 * Remove any properties that are shadowed by objects higher on the inheritance
 * chain.  Also sort the properties alphabetically.
 * @param {Array<!Array<!Object>>} data Property names from Code City.
 */
Code.ObjectPanel.filterShadowed = function(data) {
  if (!data || data.length < 2) {
    return;
  }
  var seen = Object.create(null);
  for (var datum of data) {
    var cursorInsert = 0;
    var cursorRead = 0;
    while (cursorRead < datum.length) {
      var prop = datum[cursorRead++];
      if (!seen[prop.name]) {
        seen[prop.name] = true;
        datum[cursorInsert++] = prop;
      }
    }
    datum.length = cursorInsert;
    datum.sort(Code.ObjectPanel.caseInsensitiveComp);
  }
};

/**
 * Comparison function to sort named objects A-Z without regard to case.
 * @param {string} a One string.
 * @param {string} b Another string.
 * @return {number} -1/0/1 comparator value.
 */
Code.ObjectPanel.caseInsensitiveComp = function(a, b) {
  return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
};

if (!window.TEST) {
  (function() {
    // Load the data from Code City.
    var hash = location.hash.substring(1);
    var script = document.createElement('script');
    script.src = '/code/objectPanel?parts=' + hash;
    document.head.appendChild(script);

    // Fill in the object name.
    Code.ObjectPanel.parts = JSON.parse(decodeURIComponent(hash));
    var div = document.getElementById('objectTitle');
    var lastPart = Code.ObjectPanel.parts[Code.ObjectPanel.parts.length - 1];
    var name;
    if (!lastPart) {
      name = 'Globals';
    } else if (Code.ObjectPanel.parts.length === 1) {
      // Render as 'foo' or '[42]' or '["???"]' or '^'.
      name = Code.Common.partsToSelector([lastPart]);
    } else {
      // Render as '.foo' or '[42]' or '["???"]' or '^'.
      var mockParts = [{type: 'id', value: 'X'}, lastPart];
      name = Code.Common.partsToSelector(mockParts).substring(1);
    }
    div.innerHTML = '';
    div.appendChild(document.createTextNode(name));
  })();

  window.addEventListener('load', Code.ObjectPanel.init);
}
<|endoftext|>


<commit_before>import AJV from 'ajv';
import ajvErrors from 'ajv-errors';
import { formatExtensions, frontmatterFormats, extensionFormatters } from 'Formats/formats';
import { IDENTIFIER_FIELDS } from 'Constants/fieldInference';

/**
 * Config for fields in both file and folder collections.
 */
const fieldsConfig = {
  type: 'array',
  minItems: 1,
  items: {
    // ------- Each field: -------
    type: 'object',
    properties: {
      name: { type: 'string' },
      label: { type: 'string' },
      widget: { type: 'string' },
      required: { type: 'boolean' },
    },
    required: ['name'],
  },
};

/**
 * The schema had to be wrapped in a function to
 * fix a circular dependency problem for WebPack,
 * where the imports get resolved asyncronously.
 */
const getConfigSchema = () => ({
  type: 'object',
  properties: {
    backend: {
      type: 'object',
      properties: { name: { type: 'string', examples: ['test-repo'] } },
      required: ['name'],
    },
    display_url: { type: 'string', examples: ['https://example.com'] },
    logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] },
    media_folder: { type: 'string', examples: ['assets/uploads'] },
    public_folder: { type: 'string', examples: ['/uploads'] },
    media_library: {
      type: 'object',
      properties: {
        name: { type: 'string', examples: ['uploadcare'] },
        config: { type: 'object' },
      },
      required: ['name'],
    },
    publish_mode: {
      type: 'string',
      enum: ['simple', 'editorial_workflow'],
      examples: ['editorial_workflow'],
    },
    slug: {
      type: 'object',
      properties: {
        encoding: { type: 'string', enum: ['unicode', 'ascii'] },
        clean_accents: { type: 'boolean' },
      },
    },
    collections: {
      type: 'array',
      minItems: 1,
      items: {
        // ------- Each collection: -------
        type: 'object',
        properties: {
          name: { type: 'string' },
          label: { type: 'string' },
          label_singular: { type: 'string' },
          description: { type: 'string' },
          folder: { type: 'string' },
          files: {
            type: 'array',
            items: {
              // ------- Each file: -------
              type: 'object',
              properties: {
                name: { type: 'string' },
                label: { type: 'string' },
                label_singular: { type: 'string' },
                description: { type: 'string' },
                file: { type: 'string' },
                fields: fieldsConfig,
              },
              required: ['name', 'label', 'file', 'fields'],
            },
          },
          slug: { type: 'string' },
          create: { type: 'boolean' },
          editor: {
            type: 'object',
            properties: {
              preview: { type: 'boolean' },
            },
          },
          format: { type: 'string', enum: Object.keys(formatExtensions) },
          extension: { type: 'string' },
          frontmatter_delimiter: { type: 'string' },
          fields: fieldsConfig,
        },
        required: ['name', 'label'],
        oneOf: [{ required: ['files'] }, { required: ['folder', 'fields'] }],
        if: { required: ['extension'] },
        then: {
          // Cannot infer format from extension.
          if: {
            properties: {
              extension: { enum: Object.keys(extensionFormatters) },
            },
          },
          else: { required: ['format'] },
        },
        dependencies: {
          frontmatter_delimiter: {
            properties: {
              format: { enum: frontmatterFormats },
            },
            required: ['format'],
          },
          folder: {
            errorMessage: {
              _: 'must have a field that is a valid entry identifier',
            },
            properties: {
              fields: {
                contains: {
                  properties: {
                    name: { enum: [{ $data: '3/identifier_field' }, ...IDENTIFIER_FIELDS] },
                  },
                },
              },
            },
          },
        },
      },
    },
  },
  required: ['backend', 'collections'],
  anyOf: [{ required: ['media_folder'] }, { required: ['media_library'] }],
});

class ConfigError extends Error {
  constructor(errors, ...args) {
    const message = errors
      .map(({ message, dataPath }) => {
        const dotPath = dataPath
          .slice(1)
          .split('/')
          .map(seg => (seg.match(/^\d+$/) ? `[${seg}]` : `.${seg}`))
          .join('')
          .slice(1);
        return `${dotPath ? `'${dotPath}'` : 'config'} ${message}`;
      })
      .join('\n');
    super(message, ...args);

    this.errors = errors;
    this.message = message;
  }

  toString() {
    return this.message;
  }
}

/**
 * `validateConfig` is a pure function. It does not mutate
 * the config that is passed in.
 */
export function validateConfig(config) {
  const ajv = new AJV({ allErrors: true, jsonPointers: true, $data: true });
  ajvErrors(ajv);

  const valid = ajv.validate(getConfigSchema(), config);
  if (!valid) {
    console.error('Config Errors', ajv.errors);
    throw new ConfigError(ajv.errors);
  }
}
<commit_msg>fix(config): remove identifier field validation (#1882)<commit_after>import AJV from 'ajv';
import ajvErrors from 'ajv-errors';
import { formatExtensions, frontmatterFormats, extensionFormatters } from 'Formats/formats';

/**
 * Config for fields in both file and folder collections.
 */
const fieldsConfig = {
  type: 'array',
  minItems: 1,
  items: {
    // ------- Each field: -------
    type: 'object',
    properties: {
      name: { type: 'string' },
      label: { type: 'string' },
      widget: { type: 'string' },
      required: { type: 'boolean' },
    },
    required: ['name'],
  },
};

/**
 * The schema had to be wrapped in a function to
 * fix a circular dependency problem for WebPack,
 * where the imports get resolved asyncronously.
 */
const getConfigSchema = () => ({
  type: 'object',
  properties: {
    backend: {
      type: 'object',
      properties: { name: { type: 'string', examples: ['test-repo'] } },
      required: ['name'],
    },
    display_url: { type: 'string', examples: ['https://example.com'] },
    logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] },
    media_folder: { type: 'string', examples: ['assets/uploads'] },
    public_folder: { type: 'string', examples: ['/uploads'] },
    media_library: {
      type: 'object',
      properties: {
        name: { type: 'string', examples: ['uploadcare'] },
        config: { type: 'object' },
      },
      required: ['name'],
    },
    publish_mode: {
      type: 'string',
      enum: ['simple', 'editorial_workflow'],
      examples: ['editorial_workflow'],
    },
    slug: {
      type: 'object',
      properties: {
        encoding: { type: 'string', enum: ['unicode', 'ascii'] },
        clean_accents: { type: 'boolean' },
      },
    },
    collections: {
      type: 'array',
      minItems: 1,
      items: {
        // ------- Each collection: -------
        type: 'object',
        properties: {
          name: { type: 'string' },
          label: { type: 'string' },
          label_singular: { type: 'string' },
          description: { type: 'string' },
          folder: { type: 'string' },
          files: {
            type: 'array',
            items: {
              // ------- Each file: -------
              type: 'object',
              properties: {
                name: { type: 'string' },
                label: { type: 'string' },
                label_singular: { type: 'string' },
                description: { type: 'string' },
                file: { type: 'string' },
                fields: fieldsConfig,
              },
              required: ['name', 'label', 'file', 'fields'],
            },
          },
          slug: { type: 'string' },
          create: { type: 'boolean' },
          editor: {
            type: 'object',
            properties: {
              preview: { type: 'boolean' },
            },
          },
          format: { type: 'string', enum: Object.keys(formatExtensions) },
          extension: { type: 'string' },
          frontmatter_delimiter: { type: 'string' },
          fields: fieldsConfig,
        },
        required: ['name', 'label'],
        oneOf: [{ required: ['files'] }, { required: ['folder', 'fields'] }],
        if: { required: ['extension'] },
        then: {
          // Cannot infer format from extension.
          if: {
            properties: {
              extension: { enum: Object.keys(extensionFormatters) },
            },
          },
          else: { required: ['format'] },
        },
        dependencies: {
          frontmatter_delimiter: {
            properties: {
              format: { enum: frontmatterFormats },
            },
            required: ['format'],
          },
        },
      },
    },
  },
  required: ['backend', 'collections'],
  anyOf: [{ required: ['media_folder'] }, { required: ['media_library'] }],
});

class ConfigError extends Error {
  constructor(errors, ...args) {
    const message = errors
      .map(({ message, dataPath }) => {
        const dotPath = dataPath
          .slice(1)
          .split('/')
          .map(seg => (seg.match(/^\d+$/) ? `[${seg}]` : `.${seg}`))
          .join('')
          .slice(1);
        return `${dotPath ? `'${dotPath}'` : 'config'} ${message}`;
      })
      .join('\n');
    super(message, ...args);

    this.errors = errors;
    this.message = message;
  }

  toString() {
    return this.message;
  }
}

/**
 * `validateConfig` is a pure function. It does not mutate
 * the config that is passed in.
 */
export function validateConfig(config) {
  const ajv = new AJV({ allErrors: true });
  ajvErrors(ajv);

  const valid = ajv.validate(getConfigSchema(), config);
  if (!valid) {
    console.error('Config Errors', ajv.errors);
    throw new ConfigError(ajv.errors);
  }
}
<|endoftext|>


<commit_before>$(function() {
    function getSuggestionById(mainInput, id, idKey) {
        var bloodhound = $(mainInput).data('bloodhound')
        if (!bloodhound.storage) {
            var fieldName = $(mainInput).attr('name');
            if(bloodhound.prefetch) {
                throw Error("Data not prefetched for field " + name)
            } else {
                throw Error("Prefetch config is missing for field " + name)
            }
        }
        var data = bloodhound.storage.get('data');
        for(i in data.datums) {
            if(data.datums[i][idKey] == id) {
                return data.datums[i]
            }
        }
    }
    function removeItem(e) {
        e.preventDefault()
        $(this).parents('.list-group-item').prev('input').remove()
        $(this).parents('.list-group-item').remove()
    }
    function showSelectedValue(mainInput, input, suggestion) {
        var valueKey = $(mainInput).data('valueKey');
        var labelKey = $(mainInput).data('labelKey');
        suggestion = suggestion || $(input).data('suggestion') || getSuggestionById(mainInput, $(input).prop('value'), valueKey)
        var t = _.template('<div data-id="<%= ' + valueKey + ' %>" class="list-group-item list-group-item-info fade in"><%= ' + labelKey + ' %>
');
        var item = $(t(suggestion)).insertAfter(input);
        $('a', item).click(removeItem).click(function() { $(mainInput).show().focus().val('') })
        $(mainInput).val('')
        if(!$(mainInput).prop('multiple'))
            $(mainInput).hide();
    }
    function showSelectedValuesForInput() {
        var $this = $(this);
        var name = $(this).data('name');
        var $inputs = $('input[name="' + name + '"]', $this.parents('form'));
        $inputs.each(function() {
            showSelectedValue($this, this)
        });
    }
    function onChange(e, suggestion, datasetName) {
        e.preventDefault();
        e.stopPropagation();
        var value = suggestion[$(e.target).data('valueKey')];
        var name = $(e.target).data('name')
        var isMultiple = $(e.target).prop('multiple');
        var $input = $('input[name="' + name + '"]', $(e.target).parents('form'));
        $input = makeInput(e.target, name, value)
        showSelectedValue($(e.target), $input, suggestion)
    }
    function makeInput(sourceInput, name, value) {
        return $('<input>').
            insertBefore(sourceInput).
            attr('type', 'hidden').
            attr('name', name).
            attr('value', value)
    }
    $('.typeahead').each(function() {
        $this = $(this)
        options = {
            hint: true,
            highlight: true,
            minLength: 1
        }
        var sourceConfig = $this.data('source') || null;
        if(!sourceConfig) {
            throw Error("Missing data-source attribute")
        }
        var prefetchUrl = $this.data('prefetch-url')
        var bloodhound = new Bloodhound({
            datumTokenizer: Bloodhound.tokenizers.obj.whitespace(sourceConfig.displayKey),
            queryTokenizer: Bloodhound.tokenizers.whitespace,
            prefetch: prefetchUrl ? {
                url: prefetchUrl,
                filter: sourceConfig.prefetchResponseExpression && eval("(function(response) { return " + sourceConfig.prefetchResponseExpression + ";})")
            } : null,
            remote: {
                url: sourceConfig.url,
                filter: sourceConfig.responseExpression && eval("(function(response) { return " + sourceConfig.responseExpression + ";})")
            }
        });
        var name = $this.attr('name')
        var isMultiple = $this.prop('multiple');
        $this.data('name', name)
        $this.data('valueKey', sourceConfig.valueKey)
        $this.data('labelKey', sourceConfig.displayKey)
        $this.data('bloodhound', bloodhound)
        $this.removeAttr('name')
        dataSource = {
            displayKey: sourceConfig.displayKey,
            source: bloodhound.ttAdapter(),
            templates: {
                empty: ' Nothing matches your query '
            }
        }
        if(sourceConfig.templates) {
            for(k in sourceConfig.templates) {
                dataSource.templates[k] = _.template(sourceConfig.templates[k])
            }
        }
        bloodhound.initialize().done((function($this) {
            return function(e) {
                showSelectedValuesForInput.apply($this)
            }
        })($this)
        )

        $this.typeahead(options, dataSource).on('typeahead:selected', onChange)
    })
})
<commit_msg>#80 fixing a typo<commit_after>$(function() {
    function getSuggestionById(mainInput, id, idKey) {
        var bloodhound = $(mainInput).data('bloodhound')
        if (!bloodhound.storage) {
            var name = $(mainInput).attr('name');
            if(bloodhound.prefetch) {
                throw Error("Data not prefetched for field " + name)
            } else {
                throw Error("Prefetch config is missing for field " + name)
            }
        }
        var data = bloodhound.storage.get('data');
        for(i in data.datums) {
            if(data.datums[i][idKey] == id) {
                return data.datums[i]
            }
        }
    }
    function removeItem(e) {
        e.preventDefault()
        $(this).parents('.list-group-item').prev('input').remove()
        $(this).parents('.list-group-item').remove()
    }
    function showSelectedValue(mainInput, input, suggestion) {
        var valueKey = $(mainInput).data('valueKey');
        var labelKey = $(mainInput).data('labelKey');
        suggestion = suggestion || $(input).data('suggestion') || getSuggestionById(mainInput, $(input).prop('value'), valueKey)
        var t = _.template('<div data-id="<%= ' + valueKey + ' %>" class="list-group-item list-group-item-info fade in"><%= ' + labelKey + ' %>
');
        var item = $(t(suggestion)).insertAfter(input);
        $('a', item).click(removeItem).click(function() { $(mainInput).show().focus().val('') })
        $(mainInput).val('')
        if(!$(mainInput).prop('multiple'))
            $(mainInput).hide();
    }
    function showSelectedValuesForInput() {
        var $this = $(this);
        var name = $(this).data('name');
        var $inputs = $('input[name="' + name + '"]', $this.parents('form'));
        $inputs.each(function() {
            showSelectedValue($this, this)
        });
    }
    function onChange(e, suggestion, datasetName) {
        e.preventDefault();
        e.stopPropagation();
        var value = suggestion[$(e.target).data('valueKey')];
        var name = $(e.target).data('name')
        var isMultiple = $(e.target).prop('multiple');
        var $input = $('input[name="' + name + '"]', $(e.target).parents('form'));
        $input = makeInput(e.target, name, value)
        showSelectedValue($(e.target), $input, suggestion)
    }
    function makeInput(sourceInput, name, value) {
        return $('<input>').
            insertBefore(sourceInput).
            attr('type', 'hidden').
            attr('name', name).
            attr('value', value)
    }
    $('.typeahead').each(function() {
        $this = $(this)
        options = {
            hint: true,
            highlight: true,
            minLength: 1
        }
        var sourceConfig = $this.data('source') || null;
        if(!sourceConfig) {
            throw Error("Missing data-source attribute")
        }
        var prefetchUrl = $this.data('prefetch-url')
        var bloodhound = new Bloodhound({
            datumTokenizer: Bloodhound.tokenizers.obj.whitespace(sourceConfig.displayKey),
            queryTokenizer: Bloodhound.tokenizers.whitespace,
            prefetch: prefetchUrl ? {
                url: prefetchUrl,
                filter: sourceConfig.prefetchResponseExpression && eval("(function(response) { return " + sourceConfig.prefetchResponseExpression + ";})")
            } : null,
            remote: {
                url: sourceConfig.url,
                filter: sourceConfig.responseExpression && eval("(function(response) { return " + sourceConfig.responseExpression + ";})")
            }
        });
        var name = $this.attr('name')
        var isMultiple = $this.prop('multiple');
        $this.data('name', name)
        $this.data('valueKey', sourceConfig.valueKey)
        $this.data('labelKey', sourceConfig.displayKey)
        $this.data('bloodhound', bloodhound)
        $this.removeAttr('name')
        dataSource = {
            displayKey: sourceConfig.displayKey,
            source: bloodhound.ttAdapter(),
            templates: {
                empty: ' Nothing matches your query '
            }
        }
        if(sourceConfig.templates) {
            for(k in sourceConfig.templates) {
                dataSource.templates[k] = _.template(sourceConfig.templates[k])
            }
        }
        bloodhound.initialize().done((function($this) {
            return function(e) {
                showSelectedValuesForInput.apply($this)
            }
        })($this)
        )

        $this.typeahead(options, dataSource).on('typeahead:selected', onChange)
    })
})
<|endoftext|>


<commit_before>/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

'use strict';

const {ParserError} = require('./errors');

/**
 * This FlowFixMe is supposed to refer to an InterfaceDeclaration or TypeAlias
 * declaration type. Unfortunately, we don't have those types, because flow-parser
 * generates them, and flow-parser is not type-safe. In the future, we should find
 * a way to get these types from our flow parser library.
 *
 * TODO(T71778680): Flow type AST Nodes
 */
export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe};

function getTypes(ast: $FlowFixMe): TypeDeclarationMap {
  return ast.body.reduce((types, node) => {
    if (node.type === 'ExportNamedDeclaration' && node.exportKind === 'type') {
      if (
        node.declaration.type === 'TypeAlias' ||
        node.declaration.type === 'InterfaceDeclaration'
      ) {
        types[node.declaration.id.name] = node.declaration;
      }
    } else if (
      node.type === 'ExportNamedDeclaration' &&
      node.exportKind === 'value' &&
      node.declaration &&
      node.declaration.type === 'EnumDeclaration'
    ) {
      types[node.declaration.id.name] = node.declaration;
    } else if (
      node.type === 'TypeAlias' ||
      node.type === 'InterfaceDeclaration' ||
      node.type === 'EnumDeclaration'
    ) {
      types[node.id.name] = node;
    }
    return types;
  }, {});
}

// $FlowFixMe[unclear-type] there's no flowtype for ASTs
export type ASTNode = Object;

const invariant = require('invariant');

type TypeAliasResolutionStatus =
  | $ReadOnly<{
      successful: true,
      aliasName: string,
    }>
  | $ReadOnly<{
      successful: false,
    }>;

function resolveTypeAnnotation(
  // TODO(T71778680): This is an Flow TypeAnnotation. Flow-type this
  typeAnnotation: $FlowFixMe,
  types: TypeDeclarationMap,
): {
  nullable: boolean,
  typeAnnotation: $FlowFixMe,
  typeAliasResolutionStatus: TypeAliasResolutionStatus,
} {
  invariant(
    typeAnnotation != null,
    'resolveTypeAnnotation(): typeAnnotation cannot be null',
  );

  let node = typeAnnotation;
  let nullable = false;
  let typeAliasResolutionStatus: TypeAliasResolutionStatus = {
    successful: false,
  };

  for (;;) {
    if (node.type === 'NullableTypeAnnotation') {
      nullable = true;
      node = node.typeAnnotation;
    } else if (node.type === 'GenericTypeAnnotation') {
      typeAliasResolutionStatus = {
        successful: true,
        aliasName: node.id.name,
      };
      const resolvedTypeAnnotation = types[node.id.name];
      if (
        resolvedTypeAnnotation == null ||
        resolvedTypeAnnotation.type === 'EnumDeclaration'
      ) {
        break;
      }

      invariant(
        resolvedTypeAnnotation.type === 'TypeAlias',
        `GenericTypeAnnotation '${node.id.name}' must resolve to a TypeAlias. Instead, it resolved to a '${resolvedTypeAnnotation.type}'`,
      );

      node = resolvedTypeAnnotation.right;
    } else {
      break;
    }
  }

  return {
    nullable: nullable,
    typeAnnotation: node,
    typeAliasResolutionStatus,
  };
}

function getValueFromTypes(value: ASTNode, types: TypeDeclarationMap): ASTNode {
  if (value.type === 'GenericTypeAnnotation' && types[value.id.name]) {
    return getValueFromTypes(types[value.id.name].right, types);
  }
  return value;
}

export type ParserErrorCapturer = <T>(fn: () => T) => ?T;

function createParserErrorCapturer(): [
  Array<ParserError>,
  ParserErrorCapturer,
] {
  const errors = [];
  function guard<T>(fn: () => T): ?T {
    try {
      return fn();
    } catch (error) {
      if (!(error instanceof ParserError)) {
        throw error;
      }
      errors.push(error);

      return null;
    }
  }

  return [errors, guard];
}

// TODO(T71778680): Flow-type ASTNodes.
function visit(
  astNode: $FlowFixMe,
  visitor: {
    [type: string]: (node: $FlowFixMe) => void,
  },
) {
  const queue = [astNode];
  while (queue.length !== 0) {
    let item = queue.shift();

    if (!(typeof item === 'object' && item != null)) {
      continue;
    }

    if (
      typeof item.type === 'string' &&
      typeof visitor[item.type] === 'function'
    ) {
      // Don't visit any children
      visitor[item.type](item);
    } else if (Array.isArray(item)) {
      queue.push(...item);
    } else {
      queue.push(...Object.values(item));
    }
  }
}

// TODO(T71778680): Flow-type ASTNodes.
function isModuleRegistryCall(node: $FlowFixMe): boolean {
  if (node.type !== 'CallExpression') {
    return false;
  }

  const callExpression = node;

  if (callExpression.callee.type !== 'MemberExpression') {
    return false;
  }

  const memberExpression = callExpression.callee;
  if (
    !(
      memberExpression.object.type === 'Identifier' &&
      memberExpression.object.name === 'TurboModuleRegistry'
    )
  ) {
    return false;
  }

  if (
    !(
      memberExpression.property.type === 'Identifier' &&
      (memberExpression.property.name === 'get' ||
        memberExpression.property.name === 'getEnforcing')
    )
  ) {
    return false;
  }

  if (memberExpression.computed) {
    return false;
  }

  return true;
}

module.exports = {
  getValueFromTypes,
  resolveTypeAnnotation,
  createParserErrorCapturer,
  getTypes,
  visit,
  isModuleRegistryCall,
};
<commit_msg>Fix crash when exporting type of existing variable<commit_after>/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

'use strict';

const {ParserError} = require('./errors');

/**
 * This FlowFixMe is supposed to refer to an InterfaceDeclaration or TypeAlias
 * declaration type. Unfortunately, we don't have those types, because flow-parser
 * generates them, and flow-parser is not type-safe. In the future, we should find
 * a way to get these types from our flow parser library.
 *
 * TODO(T71778680): Flow type AST Nodes
 */
export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe};

function getTypes(ast: $FlowFixMe): TypeDeclarationMap {
  return ast.body.reduce((types, node) => {
    if (node.type === 'ExportNamedDeclaration' && node.exportKind === 'type') {
      if (
        node.declaration != null &&
        (node.declaration.type === 'TypeAlias' ||
          node.declaration.type === 'InterfaceDeclaration')
      ) {
        types[node.declaration.id.name] = node.declaration;
      }
    } else if (
      node.type === 'ExportNamedDeclaration' &&
      node.exportKind === 'value' &&
      node.declaration &&
      node.declaration.type === 'EnumDeclaration'
    ) {
      types[node.declaration.id.name] = node.declaration;
    } else if (
      node.type === 'TypeAlias' ||
      node.type === 'InterfaceDeclaration' ||
      node.type === 'EnumDeclaration'
    ) {
      types[node.id.name] = node;
    }
    return types;
  }, {});
}

// $FlowFixMe[unclear-type] there's no flowtype for ASTs
export type ASTNode = Object;

const invariant = require('invariant');

type TypeAliasResolutionStatus =
  | $ReadOnly<{
      successful: true,
      aliasName: string,
    }>
  | $ReadOnly<{
      successful: false,
    }>;

function resolveTypeAnnotation(
  // TODO(T71778680): This is an Flow TypeAnnotation. Flow-type this
  typeAnnotation: $FlowFixMe,
  types: TypeDeclarationMap,
): {
  nullable: boolean,
  typeAnnotation: $FlowFixMe,
  typeAliasResolutionStatus: TypeAliasResolutionStatus,
} {
  invariant(
    typeAnnotation != null,
    'resolveTypeAnnotation(): typeAnnotation cannot be null',
  );

  let node = typeAnnotation;
  let nullable = false;
  let typeAliasResolutionStatus: TypeAliasResolutionStatus = {
    successful: false,
  };

  for (;;) {
    if (node.type === 'NullableTypeAnnotation') {
      nullable = true;
      node = node.typeAnnotation;
    } else if (node.type === 'GenericTypeAnnotation') {
      typeAliasResolutionStatus = {
        successful: true,
        aliasName: node.id.name,
      };
      const resolvedTypeAnnotation = types[node.id.name];
      if (
        resolvedTypeAnnotation == null ||
        resolvedTypeAnnotation.type === 'EnumDeclaration'
      ) {
        break;
      }

      invariant(
        resolvedTypeAnnotation.type === 'TypeAlias',
        `GenericTypeAnnotation '${node.id.name}' must resolve to a TypeAlias. Instead, it resolved to a '${resolvedTypeAnnotation.type}'`,
      );

      node = resolvedTypeAnnotation.right;
    } else {
      break;
    }
  }

  return {
    nullable: nullable,
    typeAnnotation: node,
    typeAliasResolutionStatus,
  };
}

function getValueFromTypes(value: ASTNode, types: TypeDeclarationMap): ASTNode {
  if (value.type === 'GenericTypeAnnotation' && types[value.id.name]) {
    return getValueFromTypes(types[value.id.name].right, types);
  }
  return value;
}

export type ParserErrorCapturer = <T>(fn: () => T) => ?T;

function createParserErrorCapturer(): [
  Array<ParserError>,
  ParserErrorCapturer,
] {
  const errors = [];
  function guard<T>(fn: () => T): ?T {
    try {
      return fn();
    } catch (error) {
      if (!(error instanceof ParserError)) {
        throw error;
      }
      errors.push(error);

      return null;
    }
  }

  return [errors, guard];
}

// TODO(T71778680): Flow-type ASTNodes.
function visit(
  astNode: $FlowFixMe,
  visitor: {
    [type: string]: (node: $FlowFixMe) => void,
  },
) {
  const queue = [astNode];
  while (queue.length !== 0) {
    let item = queue.shift();

    if (!(typeof item === 'object' && item != null)) {
      continue;
    }

    if (
      typeof item.type === 'string' &&
      typeof visitor[item.type] === 'function'
    ) {
      // Don't visit any children
      visitor[item.type](item);
    } else if (Array.isArray(item)) {
      queue.push(...item);
    } else {
      queue.push(...Object.values(item));
    }
  }
}

// TODO(T71778680): Flow-type ASTNodes.
function isModuleRegistryCall(node: $FlowFixMe): boolean {
  if (node.type !== 'CallExpression') {
    return false;
  }

  const callExpression = node;

  if (callExpression.callee.type !== 'MemberExpression') {
    return false;
  }

  const memberExpression = callExpression.callee;
  if (
    !(
      memberExpression.object.type === 'Identifier' &&
      memberExpression.object.name === 'TurboModuleRegistry'
    )
  ) {
    return false;
  }

  if (
    !(
      memberExpression.property.type === 'Identifier' &&
      (memberExpression.property.name === 'get' ||
        memberExpression.property.name === 'getEnforcing')
    )
  ) {
    return false;
  }

  if (memberExpression.computed) {
    return false;
  }

  return true;
}

module.exports = {
  getValueFromTypes,
  resolveTypeAnnotation,
  createParserErrorCapturer,
  getTypes,
  visit,
  isModuleRegistryCall,
};
<|endoftext|>


<commit_before>/*globals OAuth*/

const logger = new Logger('CustomOAuth');

const Services = {};
const BeforeUpdateOrCreateUserFromExternalService = [];

export class CustomOAuth {
	constructor(name, options) {
		logger.debug('Init CustomOAuth', name, options);

		this.name = name;
		if (!Match.test(this.name, String)) {
			throw new Meteor.Error('CustomOAuth: Name is required and must be String');
		}

		if (Services[this.name]) {
			Services[this.name].configure(options);
			return;
		}

		Services[this.name] = this;

		this.configure(options);

		this.userAgent = 'Meteor';
		if (Meteor.release) {
			this.userAgent += '/' + Meteor.release;
		}

		Accounts.oauth.registerService(this.name);
		this.registerService();
		this.addHookToProcessUser();
	}

	configure(options) {
		if (!Match.test(options, Object)) {
			throw new Meteor.Error('CustomOAuth: Options is required and must be Object');
		}

		if (!Match.test(options.serverURL, String)) {
			throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String');
		}

		if (!Match.test(options.tokenPath, String)) {
			options.tokenPath = '/oauth/token';
		}

		if (!Match.test(options.identityPath, String)) {
			options.identityPath = '/me';
		}

		this.serverURL = options.serverURL;
		this.tokenPath = options.tokenPath;
		this.identityPath = options.identityPath;
		this.tokenSentVia = options.tokenSentVia;
		this.usernameField = (options.usernameField || '').trim();
		this.mergeUsers = options.mergeUsers;

		if (!/^https?:\/\/.+/.test(this.tokenPath)) {
			this.tokenPath = this.serverURL + this.tokenPath;
		}

		if (!/^https?:\/\/.+/.test(this.identityPath)) {
			this.identityPath = this.serverURL + this.identityPath;
		}

		if (Match.test(options.addAutopublishFields, Object)) {
			Accounts.addAutopublishFields(options.addAutopublishFields);
		}
	}

	getAccessToken(query) {
		const config = ServiceConfiguration.configurations.findOne({service: this.name});
		if (!config) {
			throw new ServiceConfiguration.ConfigError();
		}

		let response = undefined;
		try {
			response = HTTP.post(this.tokenPath, {
				auth: config.clientId + ':' + OAuth.openSecret(config.secret),
				headers: {
					Accept: 'application/json',
					'User-Agent': this.userAgent
				},
				params: {
					code: query.code,
					client_id: config.clientId,
					client_secret: OAuth.openSecret(config.secret),
					redirect_uri: OAuth._redirectUri(this.name, config),
					grant_type: 'authorization_code',
					state: query.state
				}
			});
		} catch (err) {
			const error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}

		if (response.data.error) { //if the http response was a json object with an error attribute
			throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.data.error}`);
		} else {
			return response.data.access_token;
		}
	}

	getIdentity(accessToken) {
		const params = {};
		const headers = {
			'User-Agent': this.userAgent // http://doc.gitlab.com/ce/api/users.html#Current-user
		};

		if (this.tokenSentVia === 'header') {
			headers['Authorization'] = 'Bearer ' + accessToken;
		} else {
			params['access_token'] = accessToken;
		}

		try {
			const response = HTTP.get(this.identityPath, {
				headers: headers,
				params: params
			});

			let data;

			if (response.data) {
				data = response.data;
			} else {
				data = JSON.parse(response.content);
			}

			logger.debug('Identity response', JSON.stringify(data, null, 2));

			return data;
		} catch (err) {
			const error = new Error(`Failed to fetch identity from ${this.name} at ${this.identityPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}
	}

	registerService() {
		const self = this;
		OAuth.registerService(this.name, 2, null, (query) => {
			const accessToken = self.getAccessToken(query);
			// console.log 'at:', accessToken

			let identity = self.getIdentity(accessToken);

			if (identity) {
				// Fix for Reddit
				if (identity.result) {
					identity = identity.result;
				}

				// Fix WordPress-like identities having 'ID' instead of 'id'
				if (identity.ID && !identity.id) {
					identity.id = identity.ID;
				}

				// Fix Auth0-like identities having 'user_id' instead of 'id'
				if (identity.user_id && !identity.id) {
					identity.id = identity.user_id;
				}

				if (identity.CharacterID && !identity.id) {
					identity.id = identity.CharacterID;
				}

				// Fix Dataporten having 'user.userid' instead of 'id'
				if (identity.user && identity.user.userid && !identity.id) {
					identity.id = identity.user.userid;
					identity.email = identity.user.email;
				}

				// Fix general 'phid' instead of 'id' from phabricator
				if (identity.phid && !identity.id) {
					identity.id = identity.phid;
				}

				// Fix Keycloak-like identities having 'sub' instead of 'id'
				if (identity.sub && !identity.id) {
					identity.id = identity.sub;
				}

				// Fix general 'userid' instead of 'id' from provider
				if (identity.userid && !identity.id) {
					identity.id = identity.userid;
				}
			}

			// console.log 'id:', JSON.stringify identity, null, '  '

			const serviceData = {
				_OAuthCustom: true,
				accessToken: accessToken
			};

			_.extend(serviceData, identity);

			const data = {
				serviceData: serviceData,
				options: {
					profile: {
						name: identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name)
					}
				}
			};

			// console.log data

			return data;
		});
	}

	retrieveCredential(credentialToken, credentialSecret) {
		return OAuth.retrieveCredential(credentialToken, credentialSecret);
	}

	getUsername(data) {
		let username = '';

		if (this.usernameField.indexOf('#{') > -1) {
			username = this.usernameField.replace(/#{(.+?)}/g, function(match, field) {
				if (!data[field]) {
					throw new Meteor.Error(`Username template item "${field}" not found in data`, data);
				}
				return data[field];
			});
		} else {
			username = data[this.usernameField];
			if (!username) {
				throw new Meteor.Error(`Username field "${this.usernameField}" not found in data`, data);
			}
		}

		return username;
	}

	addHookToProcessUser() {
		BeforeUpdateOrCreateUserFromExternalService.push((serviceName, serviceData/*, options*/) => {
			if (serviceName !== this.name) {
				return;
			}

			if (this.usernameField) {
				const username = this.getUsername(serviceData);

				const user = RocketChat.models.Users.findOneByUsername(username);
				if (!user) {
					return;
				}

				if (this.mergeUsers !== true) {
					throw new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`);
				}

				const serviceIdKey = `services.${serviceName}.id`;
				const update = {
					$set: {
						[serviceIdKey]: serviceData.id
					}
				};

				RocketChat.models.Users.update({_id: user._id}, update);
			}
		});

		Accounts.validateNewUser((user) => {
			if (!user.services || !user.services[this.name] || !user.services[this.name].id) {
				return true;
			}

			if (this.usernameField) {
				user.username = this.getUsername(user.services[this.name]);
			}

			return true;
		});

	}
}

const updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
Accounts.updateOrCreateUserFromExternalService = function(/*serviceName, serviceData, options*/) {
	for (const hook of BeforeUpdateOrCreateUserFromExternalService) {
		hook.apply(this, arguments);
	}

	return updateOrCreateUserFromExternalService.apply(this, arguments);
};
<commit_msg>Fix issue creating users with username from OAuth<commit_after>/*globals OAuth*/

const logger = new Logger('CustomOAuth');

const Services = {};
const BeforeUpdateOrCreateUserFromExternalService = [];

export class CustomOAuth {
	constructor(name, options) {
		logger.debug('Init CustomOAuth', name, options);

		this.name = name;
		if (!Match.test(this.name, String)) {
			throw new Meteor.Error('CustomOAuth: Name is required and must be String');
		}

		if (Services[this.name]) {
			Services[this.name].configure(options);
			return;
		}

		Services[this.name] = this;

		this.configure(options);

		this.userAgent = 'Meteor';
		if (Meteor.release) {
			this.userAgent += '/' + Meteor.release;
		}

		Accounts.oauth.registerService(this.name);
		this.registerService();
		this.addHookToProcessUser();
	}

	configure(options) {
		if (!Match.test(options, Object)) {
			throw new Meteor.Error('CustomOAuth: Options is required and must be Object');
		}

		if (!Match.test(options.serverURL, String)) {
			throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String');
		}

		if (!Match.test(options.tokenPath, String)) {
			options.tokenPath = '/oauth/token';
		}

		if (!Match.test(options.identityPath, String)) {
			options.identityPath = '/me';
		}

		this.serverURL = options.serverURL;
		this.tokenPath = options.tokenPath;
		this.identityPath = options.identityPath;
		this.tokenSentVia = options.tokenSentVia;
		this.usernameField = (options.usernameField || '').trim();
		this.mergeUsers = options.mergeUsers;

		if (!/^https?:\/\/.+/.test(this.tokenPath)) {
			this.tokenPath = this.serverURL + this.tokenPath;
		}

		if (!/^https?:\/\/.+/.test(this.identityPath)) {
			this.identityPath = this.serverURL + this.identityPath;
		}

		if (Match.test(options.addAutopublishFields, Object)) {
			Accounts.addAutopublishFields(options.addAutopublishFields);
		}
	}

	getAccessToken(query) {
		const config = ServiceConfiguration.configurations.findOne({service: this.name});
		if (!config) {
			throw new ServiceConfiguration.ConfigError();
		}

		let response = undefined;
		try {
			response = HTTP.post(this.tokenPath, {
				auth: config.clientId + ':' + OAuth.openSecret(config.secret),
				headers: {
					Accept: 'application/json',
					'User-Agent': this.userAgent
				},
				params: {
					code: query.code,
					client_id: config.clientId,
					client_secret: OAuth.openSecret(config.secret),
					redirect_uri: OAuth._redirectUri(this.name, config),
					grant_type: 'authorization_code',
					state: query.state
				}
			});
		} catch (err) {
			const error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}

		if (response.data.error) { //if the http response was a json object with an error attribute
			throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.data.error}`);
		} else {
			return response.data.access_token;
		}
	}

	getIdentity(accessToken) {
		const params = {};
		const headers = {
			'User-Agent': this.userAgent // http://doc.gitlab.com/ce/api/users.html#Current-user
		};

		if (this.tokenSentVia === 'header') {
			headers['Authorization'] = 'Bearer ' + accessToken;
		} else {
			params['access_token'] = accessToken;
		}

		try {
			const response = HTTP.get(this.identityPath, {
				headers: headers,
				params: params
			});

			let data;

			if (response.data) {
				data = response.data;
			} else {
				data = JSON.parse(response.content);
			}

			logger.debug('Identity response', JSON.stringify(data, null, 2));

			return data;
		} catch (err) {
			const error = new Error(`Failed to fetch identity from ${this.name} at ${this.identityPath}. ${err.message}`);
			throw _.extend(error, {response: err.response});
		}
	}

	registerService() {
		const self = this;
		OAuth.registerService(this.name, 2, null, (query) => {
			const accessToken = self.getAccessToken(query);
			// console.log 'at:', accessToken

			let identity = self.getIdentity(accessToken);

			if (identity) {
				// Fix for Reddit
				if (identity.result) {
					identity = identity.result;
				}

				// Fix WordPress-like identities having 'ID' instead of 'id'
				if (identity.ID && !identity.id) {
					identity.id = identity.ID;
				}

				// Fix Auth0-like identities having 'user_id' instead of 'id'
				if (identity.user_id && !identity.id) {
					identity.id = identity.user_id;
				}

				if (identity.CharacterID && !identity.id) {
					identity.id = identity.CharacterID;
				}

				// Fix Dataporten having 'user.userid' instead of 'id'
				if (identity.user && identity.user.userid && !identity.id) {
					identity.id = identity.user.userid;
					identity.email = identity.user.email;
				}

				// Fix general 'phid' instead of 'id' from phabricator
				if (identity.phid && !identity.id) {
					identity.id = identity.phid;
				}

				// Fix Keycloak-like identities having 'sub' instead of 'id'
				if (identity.sub && !identity.id) {
					identity.id = identity.sub;
				}

				// Fix general 'userid' instead of 'id' from provider
				if (identity.userid && !identity.id) {
					identity.id = identity.userid;
				}
			}

			// console.log 'id:', JSON.stringify identity, null, '  '

			const serviceData = {
				_OAuthCustom: true,
				accessToken: accessToken
			};

			_.extend(serviceData, identity);

			const data = {
				serviceData: serviceData,
				options: {
					profile: {
						name: identity.name || identity.username || identity.nickname || identity.CharacterName || identity.userName || identity.preferred_username || (identity.user && identity.user.name)
					}
				}
			};

			// console.log data

			return data;
		});
	}

	retrieveCredential(credentialToken, credentialSecret) {
		return OAuth.retrieveCredential(credentialToken, credentialSecret);
	}

	getUsername(data) {
		let username = '';

		if (this.usernameField.indexOf('#{') > -1) {
			username = this.usernameField.replace(/#{(.+?)}/g, function(match, field) {
				if (!data[field]) {
					throw new Meteor.Error(`Username template item "${field}" not found in data`, data);
				}
				return data[field];
			});
		} else {
			username = data[this.usernameField];
			if (!username) {
				throw new Meteor.Error(`Username field "${this.usernameField}" not found in data`, data);
			}
		}

		return username;
	}

	addHookToProcessUser() {
		BeforeUpdateOrCreateUserFromExternalService.push((serviceName, serviceData/*, options*/) => {
			if (serviceName !== this.name) {
				return;
			}

			if (this.usernameField) {
				const username = this.getUsername(serviceData);

				const user = RocketChat.models.Users.findOneByUsername(username);
				if (!user) {
					return;
				}

				// User already created or merged
				if (user.services && user.services[serviceName] && user.services[serviceName].id === serviceData.id) {
					return;
				}

				if (this.mergeUsers !== true) {
					throw new Meteor.Error('CustomOAuth', `User with username ${user.username} already exists`);
				}

				const serviceIdKey = `services.${serviceName}.id`;
				const update = {
					$set: {
						[serviceIdKey]: serviceData.id
					}
				};

				RocketChat.models.Users.update({_id: user._id}, update);
			}
		});

		Accounts.validateNewUser((user) => {
			if (!user.services || !user.services[this.name] || !user.services[this.name].id) {
				return true;
			}

			if (this.usernameField) {
				user.username = this.getUsername(user.services[this.name]);
			}

			return true;
		});

	}
}

const updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
Accounts.updateOrCreateUserFromExternalService = function(/*serviceName, serviceData, options*/) {
	for (const hook of BeforeUpdateOrCreateUserFromExternalService) {
		hook.apply(this, arguments);
	}

	return updateOrCreateUserFromExternalService.apply(this, arguments);
};
<|endoftext|>


<commit_before>var server_events = (function () {

var exports = {};

var waiting_on_homeview_load = true;

var events_stored_during_tutorial = [];
var events_stored_while_loading = [];

var get_events_xhr;
var get_events_timeout;
var get_events_failures = 0;
var get_events_params = {};

function get_events_success(events) {
    var messages = [];
    var messages_to_update = [];
    var new_pointer;

    var clean_event = function clean_event (event) {
        // Only log a whitelist of the event to remove private data
        return _.pick(event, 'id', 'type', 'op');
    };

    _.each(events, function (event) {
        try {
            get_events_params.last_event_id = Math.max(get_events_params.last_event_id,
                                                       event.id);
        } catch (ex) {
            blueslip.error('Failed to update last_event_id',
                           {event: clean_event(event)},
                           ex.stack);
        }
    });

    if (tutorial.is_running()) {
        events_stored_during_tutorial = events_stored_during_tutorial.concat(events);
        return;
    }

    if (events_stored_during_tutorial.length > 0) {
        events = events_stored_during_tutorial.concat(events);
        events_stored_during_tutorial = [];
    }

    if (waiting_on_homeview_load) {
        events_stored_while_loading = events_stored_while_loading.concat(events);
        return;
    }

    if (events_stored_while_loading.length > 0) {
        events = events_stored_while_loading.concat(events);
        events_stored_while_loading = [];
    }

    var dispatch_event = function dispatch_event(event) {
        switch (event.type) {
        case 'message':
            var msg = event.message;
            msg.flags = event.flags;
            if (event.local_message_id !== undefined) {
                msg.local_id = event.local_message_id;
            }
            messages.push(msg);
            break;
        case 'pointer':
            new_pointer = event.pointer;
            break;
        case 'restart':
            reload.initiate({message: "The application has been updated; reloading!"});
            break;
        case 'update_message':
            messages_to_update.push(event);
            break;
        case 'realm':
            if (event.op === 'update' && event.property === 'name') {
                page_params.realm_name = event.value;
                notifications.redraw_title();
            }
            break;
        case 'realm_user':
            if (event.op === 'add') {
                people.add_in_realm(event.person);
            } else if (event.op === 'remove') {
                people.remove(event.person);
            } else if (event.op === 'update') {
                people.update(event.person);
            }
            break;
        case 'realm_bot':
            if (event.op === 'add') {
                bot_data.add(event.bot);
            } else if (event.op === 'remove') {
                bot_data.remove(event.bot.email);
            } else if (event.op === 'update') {
                bot_data.update(event.bot.email, event.bot);
            }
            break;
        case 'stream':
            if (event.op === 'update') {
                // Legacy: Stream properties are still managed by subs.js on the client side.
                subs.update_subscription_properties(event.name, event.property, event.value);
            }
            break;
        case 'subscription':
            if (event.op === 'add') {
                _.each(event.subscriptions, function (sub) {
                    subs.mark_subscribed(sub.name, sub);
                });
            } else if (event.op === 'remove') {
                _.each(event.subscriptions, function (rec) {
                    var sub = stream_data.get_sub_by_id(rec.stream_id);
                    subs.mark_sub_unsubscribed(sub);
                });
            } else if (event.op === 'update') {
                subs.update_subscription_properties(event.name, event.property, event.value);
            } else if (event.op === 'peer_add' || event.op === 'peer_remove') {
                _.each(event.subscriptions, function (sub) {
                    var js_event_type;
                    if (event.op === 'peer_add') {
                        js_event_type = 'peer_subscribe.zulip';

                        stream_data.add_subscriber(sub, event.user_email);
                    } else if (event.op === 'peer_remove') {
                        js_event_type = 'peer_unsubscribe.zulip';

                        stream_data.remove_subscriber(sub, event.user_email);
                    }

                    $(document).trigger(js_event_type, {stream_name: sub,
                                                        user_email: event.user_email});
                });

            }
            break;
        case 'presence':
            var users = {};
            users[event.email] = event.presence;
            activity.set_user_statuses(users, event.server_timestamp);
            break;
        case 'update_message_flags':
            var new_value = event.operation === "add";
            switch(event.flag) {
            case 'starred':
                _.each(event.messages, function (message_id) {
                    ui.update_starred(message_id, new_value);
                });
                break;
            case 'read':
                var msgs_to_update = _.map(event.messages, function (message_id) {
                    return message_store.get(message_id);
                });
                unread.mark_messages_as_read(msgs_to_update, {from: "server"});
                break;
            }
            break;
        case 'referral':
            referral.update_state(event.referrals.granted, event.referrals.used);
            break;
        case 'realm_emoji':
            emoji.update_emojis(event.realm_emoji);
            break;
        case 'alert_words':
            alert_words.words = event.alert_words;
            break;
        case 'muted_topics':
            muting_ui.handle_updates(event.muted_topics);
            break;
        case 'realm_filters':
            page_params.realm_filters = event.realm_filters;
            echo.set_realm_filters(page_params.realm_filters);
            break;
        case 'update_global_notifications':
            notifications.handle_global_notification_updates(event.notification_name,
                                                             event.setting);
            break;
        }
    };

    _.each(events, function (event) {
        try {
            dispatch_event(event);
        } catch (ex1) {
            blueslip.error('Failed to process an event\n' +
                           blueslip.exception_msg(ex1),
                           {event: clean_event(event)},
                           ex1.stack);
        }
    });

    if (messages.length !== 0) {
        try {
            messages = echo.process_from_server(messages);
            message_store.insert_new_messages(messages);
        } catch (ex2) {
            blueslip.error('Failed to insert new messages\n' +
                           blueslip.exception_msg(ex2),
                           undefined,
                           ex2.stack);
        }
    }

    if (new_pointer !== undefined
        && new_pointer > furthest_read)
    {
        furthest_read = new_pointer;
        server_furthest_read = new_pointer;
        home_msg_list.select_id(new_pointer, {then_scroll: true, use_closest: true});
    }

    if ((home_msg_list.selected_id() === -1) && !home_msg_list.empty()) {
        home_msg_list.select_id(home_msg_list.first().id, {then_scroll: false});
    }

    if (messages_to_update.length !== 0) {
        try {
            message_store.update_messages(messages_to_update);
        } catch (ex3) {
            blueslip.error('Failed to update messages\n' +
                           blueslip.exception_msg(ex3),
                           undefined,
                           ex3.stack);
        }
    }
}

function get_events(options) {
    options = _.extend({dont_block: false}, options);

    get_events_params.dont_block = options.dont_block || get_events_failures > 0;
    if (get_events_params.queue_id === undefined) {
        get_events_params.queue_id = page_params.event_queue_id;
        get_events_params.last_event_id = page_params.last_event_id;
    }

    if (get_events_xhr !== undefined) {
        get_events_xhr.abort();
    }
    if (get_events_timeout !== undefined) {
        clearTimeout(get_events_timeout);
    }
    get_events_timeout = undefined;
    get_events_xhr = channel.post({
        url:      '/json/get_events',
        data:     get_events_params,
        idempotent: true,
        timeout:  page_params.poll_timeout,
        success: function (data) {
            try {
                get_events_xhr = undefined;
                get_events_failures = 0;
                $('#connection-error').hide();

                get_events_success(data.events);
            } catch (ex) {
                blueslip.error('Failed to handle get_events success\n' +
                               blueslip.exception_msg(ex),
                               undefined,
                               ex.stack);
            }
            get_events_timeout = setTimeout(get_events, 0);
        },
        error: function (xhr, error_type, exn) {
            try {
                get_events_xhr = undefined;
                // If we are old enough to have messages outside of the
                // Tornado cache or if we're old enough that our message
                // queue has been garbage collected, immediately reload.
                if ((xhr.status === 400) &&
                    ($.parseJSON(xhr.responseText).msg.indexOf("too old") !== -1 ||
                     $.parseJSON(xhr.responseText).msg.indexOf("Bad event queue id") !== -1)) {
                    page_params.event_queue_expired = true;
                    reload.initiate({immediate: true});
                }

                if (error_type === 'abort') {
                    // Don't restart if we explicitly aborted
                    return;
                } else if (error_type === 'timeout') {
                    // Retry indefinitely on timeout.
                    get_events_failures = 0;
                    $('#connection-error').hide();
                } else {
                    get_events_failures += 1;
                }

                if (get_events_failures >= 5) {
                    $('#connection-error').show();
                } else {
                    $('#connection-error').hide();
                }
            } catch (ex) {
                blueslip.error('Failed to handle get_events error\n' +
                               blueslip.exception_msg(ex),
                               undefined,
                               ex.stack);
            }
            var retry_sec = Math.min(90, Math.exp(get_events_failures/2));
            get_events_timeout = setTimeout(get_events, retry_sec*1000);
        }
    });
}

exports.assert_get_events_running = function assert_get_events_running(error_message) {
    if (get_events_xhr === undefined && get_events_timeout === undefined) {
        exports.restart_get_events({dont_block: true});
        blueslip.error(error_message);
    }
};

exports.restart_get_events = function restart_get_events(options) {
    get_events(options);
};

exports.force_get_events = function force_get_events() {
    get_events_timeout = setTimeout(get_events, 0);
};

exports.home_view_loaded = function home_view_loaded() {
    waiting_on_homeview_load = false;
    get_events_success([]);
    $(document).trigger("home_view_loaded.zulip");
};

var watchdog_time = $.now();
setInterval(function () {
    var new_time = $.now();
    if ((new_time - watchdog_time) > 20000) { // 20 seconds.
        // Defensively reset watchdog_time here in case there's an
        // exception in one of the event handlers
        watchdog_time = new_time;
        // Our app's JS wasn't running, which probably means the machine was
        // asleep.
        $(document).trigger($.Event('unsuspend'));
    }
    watchdog_time = new_time;
}, 5000);

util.execute_early(function () {
    $(document).on('unsuspend', function () {
        // Immediately poll for new events on unsuspend
        blueslip.log("Restarting get_events due to unsuspend");
        get_events_failures = 0;
        exports.restart_get_events({dont_block: true});
    });
    get_events();
});

exports.cleanup_event_queue = function cleanup_event_queue() {
    // Submit a request to the server to cleanup our event queue
    if (page_params.event_queue_expired === true) {
        return;
    }
    channel.del({
        url:      '/json/events',
        data:     {queue_id: page_params.event_queue_id}
    });
};

window.addEventListener("beforeunload", function (event) {
    exports.cleanup_event_queue();
});

return exports;

}());
if (typeof module !== 'undefined') {
    module.exports = server_events;
}
<commit_msg>Do not save state when reloading due to an expired event queue<commit_after>var server_events = (function () {

var exports = {};

var waiting_on_homeview_load = true;

var events_stored_during_tutorial = [];
var events_stored_while_loading = [];

var get_events_xhr;
var get_events_timeout;
var get_events_failures = 0;
var get_events_params = {};

function get_events_success(events) {
    var messages = [];
    var messages_to_update = [];
    var new_pointer;

    var clean_event = function clean_event (event) {
        // Only log a whitelist of the event to remove private data
        return _.pick(event, 'id', 'type', 'op');
    };

    _.each(events, function (event) {
        try {
            get_events_params.last_event_id = Math.max(get_events_params.last_event_id,
                                                       event.id);
        } catch (ex) {
            blueslip.error('Failed to update last_event_id',
                           {event: clean_event(event)},
                           ex.stack);
        }
    });

    if (tutorial.is_running()) {
        events_stored_during_tutorial = events_stored_during_tutorial.concat(events);
        return;
    }

    if (events_stored_during_tutorial.length > 0) {
        events = events_stored_during_tutorial.concat(events);
        events_stored_during_tutorial = [];
    }

    if (waiting_on_homeview_load) {
        events_stored_while_loading = events_stored_while_loading.concat(events);
        return;
    }

    if (events_stored_while_loading.length > 0) {
        events = events_stored_while_loading.concat(events);
        events_stored_while_loading = [];
    }

    var dispatch_event = function dispatch_event(event) {
        switch (event.type) {
        case 'message':
            var msg = event.message;
            msg.flags = event.flags;
            if (event.local_message_id !== undefined) {
                msg.local_id = event.local_message_id;
            }
            messages.push(msg);
            break;
        case 'pointer':
            new_pointer = event.pointer;
            break;
        case 'restart':
            reload.initiate({message: "The application has been updated; reloading!"});
            break;
        case 'update_message':
            messages_to_update.push(event);
            break;
        case 'realm':
            if (event.op === 'update' && event.property === 'name') {
                page_params.realm_name = event.value;
                notifications.redraw_title();
            }
            break;
        case 'realm_user':
            if (event.op === 'add') {
                people.add_in_realm(event.person);
            } else if (event.op === 'remove') {
                people.remove(event.person);
            } else if (event.op === 'update') {
                people.update(event.person);
            }
            break;
        case 'realm_bot':
            if (event.op === 'add') {
                bot_data.add(event.bot);
            } else if (event.op === 'remove') {
                bot_data.remove(event.bot.email);
            } else if (event.op === 'update') {
                bot_data.update(event.bot.email, event.bot);
            }
            break;
        case 'stream':
            if (event.op === 'update') {
                // Legacy: Stream properties are still managed by subs.js on the client side.
                subs.update_subscription_properties(event.name, event.property, event.value);
            }
            break;
        case 'subscription':
            if (event.op === 'add') {
                _.each(event.subscriptions, function (sub) {
                    subs.mark_subscribed(sub.name, sub);
                });
            } else if (event.op === 'remove') {
                _.each(event.subscriptions, function (rec) {
                    var sub = stream_data.get_sub_by_id(rec.stream_id);
                    subs.mark_sub_unsubscribed(sub);
                });
            } else if (event.op === 'update') {
                subs.update_subscription_properties(event.name, event.property, event.value);
            } else if (event.op === 'peer_add' || event.op === 'peer_remove') {
                _.each(event.subscriptions, function (sub) {
                    var js_event_type;
                    if (event.op === 'peer_add') {
                        js_event_type = 'peer_subscribe.zulip';

                        stream_data.add_subscriber(sub, event.user_email);
                    } else if (event.op === 'peer_remove') {
                        js_event_type = 'peer_unsubscribe.zulip';

                        stream_data.remove_subscriber(sub, event.user_email);
                    }

                    $(document).trigger(js_event_type, {stream_name: sub,
                                                        user_email: event.user_email});
                });

            }
            break;
        case 'presence':
            var users = {};
            users[event.email] = event.presence;
            activity.set_user_statuses(users, event.server_timestamp);
            break;
        case 'update_message_flags':
            var new_value = event.operation === "add";
            switch(event.flag) {
            case 'starred':
                _.each(event.messages, function (message_id) {
                    ui.update_starred(message_id, new_value);
                });
                break;
            case 'read':
                var msgs_to_update = _.map(event.messages, function (message_id) {
                    return message_store.get(message_id);
                });
                unread.mark_messages_as_read(msgs_to_update, {from: "server"});
                break;
            }
            break;
        case 'referral':
            referral.update_state(event.referrals.granted, event.referrals.used);
            break;
        case 'realm_emoji':
            emoji.update_emojis(event.realm_emoji);
            break;
        case 'alert_words':
            alert_words.words = event.alert_words;
            break;
        case 'muted_topics':
            muting_ui.handle_updates(event.muted_topics);
            break;
        case 'realm_filters':
            page_params.realm_filters = event.realm_filters;
            echo.set_realm_filters(page_params.realm_filters);
            break;
        case 'update_global_notifications':
            notifications.handle_global_notification_updates(event.notification_name,
                                                             event.setting);
            break;
        }
    };

    _.each(events, function (event) {
        try {
            dispatch_event(event);
        } catch (ex1) {
            blueslip.error('Failed to process an event\n' +
                           blueslip.exception_msg(ex1),
                           {event: clean_event(event)},
                           ex1.stack);
        }
    });

    if (messages.length !== 0) {
        try {
            messages = echo.process_from_server(messages);
            message_store.insert_new_messages(messages);
        } catch (ex2) {
            blueslip.error('Failed to insert new messages\n' +
                           blueslip.exception_msg(ex2),
                           undefined,
                           ex2.stack);
        }
    }

    if (new_pointer !== undefined
        && new_pointer > furthest_read)
    {
        furthest_read = new_pointer;
        server_furthest_read = new_pointer;
        home_msg_list.select_id(new_pointer, {then_scroll: true, use_closest: true});
    }

    if ((home_msg_list.selected_id() === -1) && !home_msg_list.empty()) {
        home_msg_list.select_id(home_msg_list.first().id, {then_scroll: false});
    }

    if (messages_to_update.length !== 0) {
        try {
            message_store.update_messages(messages_to_update);
        } catch (ex3) {
            blueslip.error('Failed to update messages\n' +
                           blueslip.exception_msg(ex3),
                           undefined,
                           ex3.stack);
        }
    }
}

function get_events(options) {
    options = _.extend({dont_block: false}, options);

    get_events_params.dont_block = options.dont_block || get_events_failures > 0;
    if (get_events_params.queue_id === undefined) {
        get_events_params.queue_id = page_params.event_queue_id;
        get_events_params.last_event_id = page_params.last_event_id;
    }

    if (get_events_xhr !== undefined) {
        get_events_xhr.abort();
    }
    if (get_events_timeout !== undefined) {
        clearTimeout(get_events_timeout);
    }
    get_events_timeout = undefined;
    get_events_xhr = channel.post({
        url:      '/json/get_events',
        data:     get_events_params,
        idempotent: true,
        timeout:  page_params.poll_timeout,
        success: function (data) {
            try {
                get_events_xhr = undefined;
                get_events_failures = 0;
                $('#connection-error').hide();

                get_events_success(data.events);
            } catch (ex) {
                blueslip.error('Failed to handle get_events success\n' +
                               blueslip.exception_msg(ex),
                               undefined,
                               ex.stack);
            }
            get_events_timeout = setTimeout(get_events, 0);
        },
        error: function (xhr, error_type, exn) {
            try {
                get_events_xhr = undefined;
                // If we are old enough to have messages outside of the
                // Tornado cache or if we're old enough that our message
                // queue has been garbage collected, immediately reload.
                if ((xhr.status === 400) &&
                    ($.parseJSON(xhr.responseText).msg.indexOf("too old") !== -1 ||
                     $.parseJSON(xhr.responseText).msg.indexOf("Bad event queue id") !== -1)) {
                    page_params.event_queue_expired = true;
                    reload.initiate({immediate: true, save_state: false});
                }

                if (error_type === 'abort') {
                    // Don't restart if we explicitly aborted
                    return;
                } else if (error_type === 'timeout') {
                    // Retry indefinitely on timeout.
                    get_events_failures = 0;
                    $('#connection-error').hide();
                } else {
                    get_events_failures += 1;
                }

                if (get_events_failures >= 5) {
                    $('#connection-error').show();
                } else {
                    $('#connection-error').hide();
                }
            } catch (ex) {
                blueslip.error('Failed to handle get_events error\n' +
                               blueslip.exception_msg(ex),
                               undefined,
                               ex.stack);
            }
            var retry_sec = Math.min(90, Math.exp(get_events_failures/2));
            get_events_timeout = setTimeout(get_events, retry_sec*1000);
        }
    });
}

exports.assert_get_events_running = function assert_get_events_running(error_message) {
    if (get_events_xhr === undefined && get_events_timeout === undefined) {
        exports.restart_get_events({dont_block: true});
        blueslip.error(error_message);
    }
};

exports.restart_get_events = function restart_get_events(options) {
    get_events(options);
};

exports.force_get_events = function force_get_events() {
    get_events_timeout = setTimeout(get_events, 0);
};

exports.home_view_loaded = function home_view_loaded() {
    waiting_on_homeview_load = false;
    get_events_success([]);
    $(document).trigger("home_view_loaded.zulip");
};

var watchdog_time = $.now();
setInterval(function () {
    var new_time = $.now();
    if ((new_time - watchdog_time) > 20000) { // 20 seconds.
        // Defensively reset watchdog_time here in case there's an
        // exception in one of the event handlers
        watchdog_time = new_time;
        // Our app's JS wasn't running, which probably means the machine was
        // asleep.
        $(document).trigger($.Event('unsuspend'));
    }
    watchdog_time = new_time;
}, 5000);

util.execute_early(function () {
    $(document).on('unsuspend', function () {
        // Immediately poll for new events on unsuspend
        blueslip.log("Restarting get_events due to unsuspend");
        get_events_failures = 0;
        exports.restart_get_events({dont_block: true});
    });
    get_events();
});

exports.cleanup_event_queue = function cleanup_event_queue() {
    // Submit a request to the server to cleanup our event queue
    if (page_params.event_queue_expired === true) {
        return;
    }
    channel.del({
        url:      '/json/events',
        data:     {queue_id: page_params.event_queue_id}
    });
};

window.addEventListener("beforeunload", function (event) {
    exports.cleanup_event_queue();
});

return exports;

}());
if (typeof module !== 'undefined') {
    module.exports = server_events;
}
<|endoftext|>


<commit_before>// ==========================================================================
// Project:  SproutCore Runtime
// Copyright: ©2011 Strobe Inc. and contributors.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

var get = SC.get;
var set = SC.set;

SC.Gestures = SC.Object.create({

  _registeredGestures: null,

  init: function() {
    this._registeredGestures = {};

    return this._super();
  },

  register: function(name, recognizer) {
    var registeredGestures = this._registeredGestures;

    if (registeredGestures[name] !== undefined) {
      throw new SC.Error(name+" already exists as a registered gesture recognizers. Gesture recognizers must have globally unique names.");
    }

    registeredGestures[name] = recognizer;

    this._attachListeners(recognizer, name);
  },

  knownGestures: function() {
    var registeredGestures = this._registeredGestures;

    return (registeredGestures)? registeredGestures : {};
  },

  _attachListeners: function(recognizer, name) {
    var events = ['start','move','end','cancel'];

    for (var i=0, l=events.length; i<l; i++) {
      var event = events[i];
      this._setupHandler(recognizer, name, event);
    }
  },

  _setupHandler: function(recognizer, name, event) {
    var that = this;

    console.log('$(document.body).delegate(".'+name+'", "touch'+event+'", function(e){');
    $(document.body).delegate("."+name, "touch"+event, function(e){
      console.log('hever');
      that.eventFired(recognizer,eventName,e);
    })
  },

  eventFired: function(recognizer, eventName, evt) {
    console.log('Event',eventName,'fired on recognizer',recognizer);
  }

});

SC.GestureSupport = SC.Mixin.create({
  isGesturable: true,
  name: null,
  state: null,

  distance: function(first, second) {

    var x = first.pageX;
    var y = first.pageY;
    var x0 = second.pageX;
    var y0 = second.pageY;

    return Math.sqrt((x -= x0) * x + (y -= y0) * y);
  },

  toString: function() {
    return this._super()+' with SC.GestureSupport';
  },

  touchStart: function(evt) {
    console.log('touchstart in mixin');
  },

  touchMove: function(evt) {
    console.log('touchmove in mixin');
  },

  touchEnd: function(evt) {
    console.log('touchend in mixin');

  }
});

SC.Gestures.POSSIBLE_STATE = 0;
SC.Gestures.BEGAN_STATE = 1;
SC.Gestures.CHANGED_STATE = 2;
SC.Gestures.ENDED_STATE = 3;
SC.Gestures.CANCELLED_STATE = 4;

/**

  If there are two touches
    at start, calculate distance
    when the touches move, recalculate distance
    calculate scale from change in distance

 */
SC.PinchGestureRecognizer = SC.Object.extend(SC.GestureSupport, {
  numberOfTouches: 2,

  _currentDistanceBetweenTouches: null,
  _previousDistanceBetweenTouches: null,
  _scaleThreshold: 0,

  scale: 0,

  start: function(evt) {
    console.group('Pinch');
  },

  move: function(evt) {

  },

  end: function(evt) {
    this._state = SC.Gestures.ENDED_STATE;

    console.groupEnd();
  },

  cancel: function(evt) {
    this._state = SC.Gestures.CANCELLED_STATE;

    console.groupEnd();
  }
});

SC.Gestures.register('pinch', SC.PinchGestureRecognizer);
<commit_msg>Clean up some gesture support code<commit_after>// ==========================================================================
// Project:  SproutCore Runtime
// Copyright: ©2011 Strobe Inc. and contributors.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

var get = SC.get;
var set = SC.set;

SC.Gestures = SC.Object.create({

  _registeredGestures: null,

  init: function() {
    this._registeredGestures = {};

    return this._super();
  },

  register: function(name, recognizer) {
    var registeredGestures = this._registeredGestures;

    if (registeredGestures[name] !== undefined) {
      throw new SC.Error(name+" already exists as a registered gesture recognizers. Gesture recognizers must have globally unique names.");
    }

    registeredGestures[name] = recognizer;

    this._attachListeners(recognizer, name);
  },

  knownGestures: function() {
    var registeredGestures = this._registeredGestures;

    return (registeredGestures)? registeredGestures : {};
  },

  _attachListeners: function(recognizer, name) {
    var events = ['start','move','end','cancel'];

    for (var i=0, l=events.length; i<l; i++) {
      var event = events[i];
      this._setupHandler(recognizer, name, event);
    }
  },

  _setupHandler: function(recognizer, name, event) {
    var that = this;

    console.log('$(document.body).delegate(".'+name+'", "touch'+event+'", function(e){');
    $(document.body).delegate("."+name, "touch"+event, function(e){
      console.log('hever');
      that.eventFired(recognizer,eventName,e);
    })
  },

  eventFired: function(recognizer, eventName, evt) {
    console.log('Event',eventName,'fired on recognizer',recognizer);
  }

});

SC.GestureSupport = SC.Mixin.create({
  isGesturable: true,
  name: null,
  state: null,

  distance: function(first, second) {

    var x = first.pageX;
    var y = first.pageY;
    var x0 = second.pageX;
    var y0 = second.pageY;

    return Math.sqrt((x -= x0) * x + (y -= y0) * y);
  },

  toString: function() {
    return this._super()+' with SC.GestureSupport';
  }
});

SC.Gestures.POSSIBLE_STATE = 0;
SC.Gestures.BEGAN_STATE = 1;
SC.Gestures.CHANGED_STATE = 2;
SC.Gestures.ENDED_STATE = 3;
SC.Gestures.CANCELLED_STATE = 4;

/**

  If there are two touches
    at start, calculate distance
    when the touches move, recalculate distance
    calculate scale from change in distance

 */
SC.PinchGestureRecognizer = SC.Object.extend(SC.GestureSupport, {
  numberOfTouches: 2,

  _currentDistanceBetweenTouches: null,
  _previousDistanceBetweenTouches: null,
  _scaleThreshold: 0,

  scale: 0,

  start: function(evt) {
    console.group('Pinch');
  },

  move: function(evt) {

  },

  end: function(evt) {
    this._state = SC.Gestures.ENDED_STATE;

    console.groupEnd();
  },

  cancel: function(evt) {
    this._state = SC.Gestures.CANCELLED_STATE;

    console.groupEnd();
  }
});

SC.Gestures.register('pinch', SC.PinchGestureRecognizer);
<|endoftext|>