<commit_before>import { opModZ } from '../spacegroups/spacegroups';
import { buildDSymbol } from '../dsymbols/delaney';
import { minimal } from '../dsymbols/derived';
import * as common from './common';

import {
  coordinateChangesQ as opsQ,
  coordinateChangesF as opsF
} from '../geometry/types';

const mapFace = (face, toPrimitive) =>
  face.map(p => opsF.times(toPrimitive, opsF.point(p)));

const applyOpsToPoint = (p, offset, symOps, pointsEqFn) => {
  const inStabilizer = op => pointsEqFn(p, common.applyToPoint(op, p));
  const reps = common.cosetReps(symOps, inStabilizer);

  const images = {};

  for (let i = 0; i < reps.length; ++i) {
    for (const op of common.subgroup(symOps, inStabilizer))
      images[opModZ(opsQ.times(reps[i], op))] = offset + i;
  }

  const result = [];

  for (const r of reps) {
    const point = opsF.modZ(common.applyToPoint(r, p));

    const action = {};
    for (const op of symOps)
      action[op] = images[opModZ(opsQ.times(op, r))];

    result.push([ point, action ]);
  }

  return result;
};

const applyOpsToCorners = (rawFaces, symOps, pointsEqFn) => {
  const pos = [];
  const action = [];
  const faces = [];

  for (const rf of rawFaces) {
    const face = [];

    for (const p of rf) {
      const found = pos.findIndex(q => pointsEqFn(p, q));
      const index = found < 0 ? pos.length : found;

      if (found < 0) {
        for (const [p, a] of applyOpsToPoint(p, index, symOps, pointsEqFn)) {
          pos.push(p);
          action.push(a);
        }
      }

      const shift = opsF.minus(p, pos[index]).map(x => opsF.round(x));
      face.push({ index, shift });
    }

    faces.push(face);
  }

  return { pos, action, faces };
};

const compareFaces = (f1, f2) => {
  for (const i in f1) {
    const d = f1[i].index - f2[i].index;
    if (d != 0)
      return d;

    for (const j in f1[i].shift) {
      const d = f1[i].shift[j] - f2[i].shift[j];
      if (d != 0)
        return d;
    }
  }
  return false;
};

const normalizedFace = face => {
  let best = null;
  let bestShift = null;

  for (const i in face) {
    const s = face[i].shift;
    const fa = face.slice(i).concat(face.slice(0, i))
      .map(({ index, shift }) => ({ index, shift: opsF.minus(shift, s) }));
    const fb = [fa[0]].concat(fa.slice(1).reverse());

    if (best == null || compareFaces(fa, best) < 0) {
      best = fa;
      bestShift = s;
    }
    if (compareFaces(fb, best) < 0) {
      best = fb;
      bestShift = s;
    }
  }

  return { face: best, shift: bestShift };
};

const normalizedTile = tile => {
  const cmpPairs = (p, q) => (
    compareFaces(p.face, q.face) || opsF.cmp(p.shift, q.shift)
  );

  const cmpTiles = (ps, qs) => {
    for (let i = 0; i < ps.length; ++i) {
      const d = cmpPairs(ps[i], qs[i]);
      if (d)
        return d;
    }
    return 0;
  };

  let best = null;

  for (const { shift: s0 } of tile) {
    const mapped = (
      tile
        .map(({ face, shift }) => ({ face, shift: opsF.minus(shift, s0) }))
        .sort(cmpPairs)
    );
    if (best == null || cmpTiles(best, mapped) < 0)
      best = mapped;
  }

  return best;
};

const cornerAction = (pos, action) => (op, { index, shift }) => {
  const oldPos = opsF.plus(pos[index], shift);
  const newPos = common.applyToPoint(op, oldPos);
  const newIndex = action[index][op];

  return {
    index: newIndex,
    shift: opsF.minus(newPos, pos[newIndex]).map(x => opsF.round(x))
  }
};

const applyOpsToFaces = ({ pos, action, faces }, symOps) => {
  const apply = cornerAction(pos, action);
  const seen = {};
  const result = [];

  for (const f of faces) {
    for (const op of symOps) {
      const { face: fNew } = normalizedFace(f.map(e => apply(op, e)));
      const key = JSON.stringify(fNew);

      if (!seen[key]) {
        seen[key] = true;
        result.push(fNew);
      }
    }
  }

  return result;
};

const applyOpsToTiles = ({ pos, action, faces }, tiles, symOps) => {
  const apply = cornerAction(pos, action);
  const seen = {};
  const result = [];

  for (const t of tiles) {
    for (const op of symOps) {
      const mappedFaces = t.map(
        i => normalizedFace(faces[i].map(e => apply(op, e)))
      );
      const tNew = normalizedTile(mappedFaces);
      const key = JSON.stringify(tNew);

      if (!seen[key]) {
        seen[key] = true;
        result.push(tNew);
      }
    }
  }

  return result;
};

const normalized = v => opsF.div(v, opsF.norm(v));

const sectorNormals = vs => {
  const centroid = opsF.div(vs.reduce((v, w) => opsF.plus(v, w)), vs.length);

  return vs.map((v, i) => {
    const w = vs[(i + 1) % vs.length];
    return normalized(
      opsF.crossProduct(opsF.minus(w, v), opsF.minus(centroid, w)));
  });
};

const collectEdges = faces => {
  const facesAtEdge = {};

  faces.forEach((face, i) => {
    const n = face.length;
    const edges = face.map((v, i) => [v, face[(i + 1) % n]]);

    edges.forEach(([{index: v1, shift: s1}, {index: v2, shift: s2}], j) => {
      const key = JSON.stringify([v1, v2, opsF.minus(s2, s1)]);
      const keyInv = JSON.stringify([v2, v1, opsF.minus(s1, s2)]);

      if (facesAtEdge[key])
        facesAtEdge[key].push([i, j, false]);
      else if (facesAtEdge[keyInv])
        facesAtEdge[keyInv].push([i, j, true]);
      else
        facesAtEdge[key] = [[i, j, false]];
    });
  });

  return facesAtEdge;
};

const collectTileEdges = tile => {
  const facesAtEdge = {};

  tile.forEach(({ face, shift }, i) => {
    const n = face.length;
    const edges = face.map((v, i) => [v, face[(i + 1) % n]]);

    edges.forEach(([{index: v1, shift: s1}, {index: v2, shift: s2}], j) => {
      const key = JSON.stringify(
        [v1, v2, opsF.minus(s2, s1), opsF.plus(shift, s1)]
      );
      const keyInv = JSON.stringify(
        [v2, v1, opsF.minus(s1, s2), opsF.plus(shift, s2)]
      );

      if (facesAtEdge[key])
        facesAtEdge[key].push([i, j, false]);
      else if (facesAtEdge[keyInv])
        facesAtEdge[keyInv].push([i, j, true]);
      else
        facesAtEdge[key] = [[i, j, false]];
    });
  });

  return facesAtEdge;
};

const tilingBase = faces => {
  const pairings = [[], [], [], []];
  const faceOffsets = [];
  let offset = 1;

  for (const face of faces) {
    const n = face.length;

    for (let i = 0; i < 4 * n; i += 2)
      pairings[0].push([offset + i, offset + i + 1]);

    for (let i = 1; i < 2 * n; i += 2) {
      const i1 = (i + 1) % (2 * n);
      pairings[1].push([offset + i, offset + i1]);
      pairings[1].push([offset + i + 2 * n, offset + i1 + 2 * n]);
    }

    for (let i = 0; i < 2 * n; ++i)
      pairings[3].push([offset + i, offset + i + 2 * n]);

    faceOffsets.push(offset);
    offset += 4 * n;
  }

  return { pairings, faceOffsets, size: offset - 1 };
};

const op2PairingsForPlainMode = (corners, faces, offsets) => {
  const explicitFaces = faces.map(f => f.map(
    item => opsF.plus(opsF.vector(corners[item.index]), item.shift)));

  const normals = explicitFaces.map(sectorNormals)
  const facesAtEdge = collectEdges(faces);
  const getNormal = ([i, j, rev]) => opsF.times(normals[i][j], rev ? -1 : 1);

  const result = [];
  for (const key of Object.keys(facesAtEdge)) {
    const [v, w, s] = JSON.parse(key);
    const d = normalized(opsF.minus(opsF.plus(corners[w], s), corners[v]));
    const n0 = getNormal(facesAtEdge[key][0]);

    const incidences = facesAtEdge[key].map(([i, j, reverse]) => {
      const n = getNormal([i, j, reverse]);
      const angle = Math.acos(Math.max(-1, Math.min(1, opsF.times(n0, n))));

      if (opsF.determinant([d, n0, n]) < 0)
        return [i, j, reverse, 2 * Math.PI - angle];
      else
        return [i, j, reverse, angle];
    });

    incidences.sort((a, b) => a[3] - b[3]);
    const m = incidences.length;

    incidences.forEach(([face1, edge1, rev1, angle1], i) => {
      const [face2, edge2, rev2, angle2] = incidences[(i + 1) % m];
      const [offset1, size1] = [offsets[face1], faces[face1].length];
      const [offset2, size2] = [offsets[face2], faces[face2].length];

      let a, b, c, d;

      if (rev1) {
        const k = offset1 + 2 * (size1 + edge1);
        a = k + 1;
        b = k;
      } else {
        const k = offset1 + 2 * edge1;
        a = k;
        b = k + 1;
      }

      if (rev2) {
        const k = offset2 + 2 * edge2;
        c = k + 1;
        d = k;
      } else {
        const k = offset2 + 2 * (size2 + edge2);
        c = k;
        d = k + 1;
      }

      result.push([a, c]);
      result.push([b, d]);
    });
  }

  return result;
};

const op2PairingsForTileMode = (faces, offsets, tiles) => {
  const tilesAtFace = {};

  for (let i = 0; i < tiles.length; ++i) {
    for (const { face, shift } of tiles[i]) {
      const key = JSON.stringify(face);
      if (tilesAtFace[key] == null)
        tilesAtFace[key] = [];
      tilesAtFace[key].push([i, shift]);
    }
  }

  for (const key in tilesAtFace) {
    const n = tilesAtFace[key].length;
    if (n != 2)
      throw new Error(`Face is incident to ${n} tile(s).`);
  }

  for (let tIdx = 0; tIdx < tiles.length; ++tIdx) {
    const tile = tiles[tIdx];
    const facesAtEdge = collectTileEdges(tile);

    for (const key of Object.keys(facesAtEdge)) {
      const flist = facesAtEdge[key];
      if (flist.length != 2)
        throw new Error(`tile edge incident to ${flist.length} edge(s)`);

      const [[Da, Ea], [Db, Eb]] = flist.map(([fIdx, eIdx, rev]) => {
        const { face, shift } = tile[fIdx];
        const taf = tilesAtFace[JSON.stringify(face)];

        let t;
        if (taf[0][0] == tIdx && opsQ.eq(taf[0][1], shift))
          t = 0;
        else if (taf[1][0] == tIdx && opsQ.eq(taf[1][1], shift))
          t = 1;
        else
          throw new Error(`face-tile inconsistency`);

        return [ null, null ];
      });
    }
  }
};

const makeDSymbol = (dim, size, pairings) => {
  const s = pairings.map(p => {
    const r = [];
    for (const [D, E] of p) {
      r[D] = E;
      r[E] = D;
    }
    return r;
  });

  return buildDSymbol({
    dim,
    size,
    getS: (i, D) => s[i][D],
    getV: (i, D) => 1
  });
};

export const tilingFromFacelist = spec => {
  const { name, group } = spec;
  const { warnings, errors, ops, toPrimitive, gram } = common.fromSpec(spec);

  const pointsEq = common.pointsAreCloseModZ(gram, 0.001);

  const facesIn = spec.faces.map(face => mapFace(face, toPrimitive));
  const cornerData = applyOpsToCorners(facesIn, ops, pointsEq);
  const faces = applyOpsToFaces(cornerData, ops);

  const { pairings, faceOffsets, size } = tilingBase(faces);

  if (spec.tiles.length == 0)
    pairings[2] = op2PairingsForPlainMode(cornerData.pos, faces, faceOffsets);
  else {
    const tiles = applyOpsToTiles(cornerData, spec.tiles, ops);
    pairings[2] = op2PairingsForTileMode(faces, faceOffsets, tiles);
  }

  const cover = makeDSymbol(3, size, pairings);
  const symbol = minimal(cover);

  // TODO include original vertex positions in output

  return { name, group: group.name, symbol, cover, warnings, errors };
};
<commit_msg>faceList: tweak some helpers<commit_after>import { opModZ } from '../spacegroups/spacegroups';
import { buildDSymbol } from '../dsymbols/delaney';
import { minimal } from '../dsymbols/derived';
import * as common from './common';

import {
  coordinateChangesQ as opsQ,
  coordinateChangesF as opsF
} from '../geometry/types';

const mapFace = (face, toPrimitive) =>
  face.map(p => opsF.times(toPrimitive, opsF.point(p)));

const applyOpsToPoint = (p, offset, symOps, pointsEqFn) => {
  const inStabilizer = op => pointsEqFn(p, common.applyToPoint(op, p));
  const sub = common.subgroup(symOps, inStabilizer);
  const reps = common.cosetReps(symOps, inStabilizer);

  const images = {};

  for (let i = 0; i < reps.length; ++i) {
    for (const op of sub)
      images[opModZ(opsQ.times(reps[i], op))] = offset + i;
  }

  const result = [];

  for (const r of reps) {
    const point = opsF.modZ(common.applyToPoint(r, p));

    const action = {};
    for (const op of symOps)
      action[op] = images[opModZ(opsQ.times(op, r))];

    result.push([ point, action ]);
  }

  return result;
};

const applyOpsToCorners = (rawFaces, symOps, pointsEqFn) => {
  const pos = [];
  const action = [];
  const faces = [];

  for (const rf of rawFaces) {
    const face = [];

    for (const p of rf) {
      const found = pos.findIndex(q => pointsEqFn(p, q));
      const index = found < 0 ? pos.length : found;

      if (found < 0) {
        for (const [p, a] of applyOpsToPoint(p, index, symOps, pointsEqFn)) {
          pos.push(p);
          action.push(a);
        }
      }

      const shift = opsF.minus(p, pos[index]).map(x => opsF.round(x));
      face.push({ index, shift });
    }

    faces.push(face);
  }

  return { pos, action, faces };
};

const compareFaces = (f1, f2) => {
  for (const i in f1) {
    const d = (
      opsF.cmp(f1[i].index, f2[i].index) ||
        opsF.cmp(f1[i].shift, f2[i].shift)
    );
    if (d != 0)
      return d;
  }
  return 0;
};

const normalizedFace = face => {
  let best = null;
  let bestShift = null;

  for (const i in face) {
    const s = face[i].shift;
    const fs = face.map(
      ({ index, shift }) => ({ index, shift: opsF.minus(shift, s) })
    );

    const fa = fs.slice(i).concat(fs.slice(0, i))
    const fb = [fa[0]].concat(fa.slice(1).reverse());

    if (best == null || compareFaces(fa, best) < 0) {
      best = fa;
      bestShift = s;
    }
    if (compareFaces(fb, best) < 0) {
      best = fb;
      bestShift = s;
    }
  }

  return { face: best, shift: bestShift };
};

const normalizedTile = tile => {
  const cmpPairs = (p, q) =>
    compareFaces(p.face, q.face) || opsF.cmp(p.shift, q.shift);

  const cmpTiles = (ps, qs) => {
    for (let i = 0; i < ps.length; ++i) {
      const d = cmpPairs(ps[i], qs[i]);
      if (d)
        return d;
    }
    return 0;
  };

  let best = null;

  for (const { shift: s0 } of tile) {
    const mapped = tile.map(
      ({ face, shift }) => ({ face, shift: opsF.minus(shift, s0) })
    );
    mapped.sort(cmpPairs)

    if (best == null || cmpTiles(best, mapped) < 0)
      best = mapped;
  }

  return best;
};

const cornerAction = (pos, action) => (op, c) => {
  const newPos = common.applyToPoint(op, opsF.plus(pos[c.index], c.shift));
  const index = action[c.index][op];
  const shift = opsF.minus(newPos, pos[index]).map(x => opsF.round(x));

  return { index, shift };
};

const applyOpsToFaces = ({ pos, action, faces }, symOps) => {
  const apply = cornerAction(pos, action);
  const seen = {};
  const result = [];

  for (const f of faces) {
    for (const op of symOps) {
      const { face: fNew } = normalizedFace(f.map(e => apply(op, e)));
      const key = JSON.stringify(fNew);

      if (!seen[key]) {
        seen[key] = true;
        result.push(fNew);
      }
    }
  }

  return result;
};

const applyOpsToTiles = ({ pos, action, faces }, tiles, symOps) => {
  const apply = cornerAction(pos, action);
  const seen = {};
  const result = [];

  for (const t of tiles) {
    for (const op of symOps) {
      const mappedFaces = t.map(
        i => normalizedFace(faces[i].map(e => apply(op, e)))
      );
      const tNew = normalizedTile(mappedFaces);
      const key = JSON.stringify(tNew);

      if (!seen[key]) {
        seen[key] = true;
        result.push(tNew);
      }
    }
  }

  return result;
};

const normalized = v => opsF.div(v, opsF.norm(v));

const sectorNormals = vs => {
  const centroid = opsF.div(vs.reduce((v, w) => opsF.plus(v, w)), vs.length);

  return vs.map((v, i) => {
    const w = vs[(i + 1) % vs.length];
    return normalized(
      opsF.crossProduct(opsF.minus(w, v), opsF.minus(centroid, w)));
  });
};

const collectEdges = faces => {
  const facesAtEdge = {};

  faces.forEach((face, i) => {
    const n = face.length;
    const edges = face.map((v, i) => [v, face[(i + 1) % n]]);

    edges.forEach(([{index: v1, shift: s1}, {index: v2, shift: s2}], j) => {
      const key = JSON.stringify([v1, v2, opsF.minus(s2, s1)]);
      const keyInv = JSON.stringify([v2, v1, opsF.minus(s1, s2)]);

      if (facesAtEdge[key])
        facesAtEdge[key].push([i, j, false]);
      else if (facesAtEdge[keyInv])
        facesAtEdge[keyInv].push([i, j, true]);
      else
        facesAtEdge[key] = [[i, j, false]];
    });
  });

  return facesAtEdge;
};

const collectTileEdges = tile => {
  const facesAtEdge = {};

  tile.forEach(({ face, shift }, i) => {
    const n = face.length;
    const edges = face.map((v, i) => [v, face[(i + 1) % n]]);

    edges.forEach(([{index: v1, shift: s1}, {index: v2, shift: s2}], j) => {
      const key = JSON.stringify(
        [v1, v2, opsF.minus(s2, s1), opsF.plus(shift, s1)]
      );
      const keyInv = JSON.stringify(
        [v2, v1, opsF.minus(s1, s2), opsF.plus(shift, s2)]
      );

      if (facesAtEdge[key])
        facesAtEdge[key].push([i, j, false]);
      else if (facesAtEdge[keyInv])
        facesAtEdge[keyInv].push([i, j, true]);
      else
        facesAtEdge[key] = [[i, j, false]];
    });
  });

  return facesAtEdge;
};

const tilingBase = faces => {
  const pairings = [[], [], [], []];
  const faceOffsets = [];
  let offset = 1;

  for (const face of faces) {
    const n = face.length;

    for (let i = 0; i < 4 * n; i += 2)
      pairings[0].push([offset + i, offset + i + 1]);

    for (let i = 1; i < 2 * n; i += 2) {
      const i1 = (i + 1) % (2 * n);
      pairings[1].push([offset + i, offset + i1]);
      pairings[1].push([offset + i + 2 * n, offset + i1 + 2 * n]);
    }

    for (let i = 0; i < 2 * n; ++i)
      pairings[3].push([offset + i, offset + i + 2 * n]);

    faceOffsets.push(offset);
    offset += 4 * n;
  }

  return { pairings, faceOffsets, size: offset - 1 };
};

const op2PairingsForPlainMode = (corners, faces, offsets) => {
  const explicitFaces = faces.map(f => f.map(
    item => opsF.plus(opsF.vector(corners[item.index]), item.shift)));

  const normals = explicitFaces.map(sectorNormals)
  const facesAtEdge = collectEdges(faces);
  const getNormal = ([i, j, rev]) => opsF.times(normals[i][j], rev ? -1 : 1);

  const result = [];
  for (const key of Object.keys(facesAtEdge)) {
    const [v, w, s] = JSON.parse(key);
    const d = normalized(opsF.minus(opsF.plus(corners[w], s), corners[v]));
    const n0 = getNormal(facesAtEdge[key][0]);

    const incidences = facesAtEdge[key].map(([i, j, reverse]) => {
      const n = getNormal([i, j, reverse]);
      const angle = Math.acos(Math.max(-1, Math.min(1, opsF.times(n0, n))));

      if (opsF.determinant([d, n0, n]) < 0)
        return [i, j, reverse, 2 * Math.PI - angle];
      else
        return [i, j, reverse, angle];
    });

    incidences.sort((a, b) => a[3] - b[3]);
    const m = incidences.length;

    incidences.forEach(([face1, edge1, rev1, angle1], i) => {
      const [face2, edge2, rev2, angle2] = incidences[(i + 1) % m];
      const [offset1, size1] = [offsets[face1], faces[face1].length];
      const [offset2, size2] = [offsets[face2], faces[face2].length];

      let a, b, c, d;

      if (rev1) {
        const k = offset1 + 2 * (size1 + edge1);
        a = k + 1;
        b = k;
      } else {
        const k = offset1 + 2 * edge1;
        a = k;
        b = k + 1;
      }

      if (rev2) {
        const k = offset2 + 2 * edge2;
        c = k + 1;
        d = k;
      } else {
        const k = offset2 + 2 * (size2 + edge2);
        c = k;
        d = k + 1;
      }

      result.push([a, c]);
      result.push([b, d]);
    });
  }

  return result;
};

const op2PairingsForTileMode = (faces, offsets, tiles) => {
  const tilesAtFace = {};

  for (let i = 0; i < tiles.length; ++i) {
    for (const { face, shift } of tiles[i]) {
      const key = JSON.stringify(face);
      if (tilesAtFace[key] == null)
        tilesAtFace[key] = [];
      tilesAtFace[key].push([i, shift]);
    }
  }

  for (const key in tilesAtFace) {
    const n = tilesAtFace[key].length;
    if (n != 2)
      throw new Error(`Face is incident to ${n} tile(s).`);
  }

  for (let tIdx = 0; tIdx < tiles.length; ++tIdx) {
    const tile = tiles[tIdx];
    const facesAtEdge = collectTileEdges(tile);

    for (const key of Object.keys(facesAtEdge)) {
      const flist = facesAtEdge[key];
      if (flist.length != 2)
        throw new Error(`tile edge incident to ${flist.length} edge(s)`);

      const [[Da, Ea], [Db, Eb]] = flist.map(([fIdx, eIdx, rev]) => {
        const { face, shift } = tile[fIdx];
        const taf = tilesAtFace[JSON.stringify(face)];

        let t;
        if (taf[0][0] == tIdx && opsQ.eq(taf[0][1], shift))
          t = 0;
        else if (taf[1][0] == tIdx && opsQ.eq(taf[1][1], shift))
          t = 1;
        else
          throw new Error(`face-tile inconsistency`);

        return [ null, null ];
      });
    }
  }
};

const makeDSymbol = (dim, size, pairings) => {
  const s = pairings.map(p => {
    const r = [];
    for (const [D, E] of p) {
      r[D] = E;
      r[E] = D;
    }
    return r;
  });

  return buildDSymbol({
    dim,
    size,
    getS: (i, D) => s[i][D],
    getV: (i, D) => 1
  });
};

export const tilingFromFacelist = spec => {
  const { name, group } = spec;
  const { warnings, errors, ops, toPrimitive, gram } = common.fromSpec(spec);

  const pointsEq = common.pointsAreCloseModZ(gram, 0.001);

  const facesIn = spec.faces.map(face => mapFace(face, toPrimitive));
  const cornerData = applyOpsToCorners(facesIn, ops, pointsEq);
  const faces = applyOpsToFaces(cornerData, ops);

  const { pairings, faceOffsets, size } = tilingBase(faces);

  if (spec.tiles.length == 0)
    pairings[2] = op2PairingsForPlainMode(cornerData.pos, faces, faceOffsets);
  else {
    const tiles = applyOpsToTiles(cornerData, spec.tiles, ops);
    pairings[2] = op2PairingsForTileMode(faces, faceOffsets, tiles);
  }

  const cover = makeDSymbol(3, size, pairings);
  const symbol = minimal(cover);

  // TODO include original vertex positions in output

  return { name, group: group.name, symbol, cover, warnings, errors };
};
<|endoftext|>


<commit_before>'use strict';

var fileExists = require('file-exists');

var fs = require('fs');

var rw = require('./rw')
	, read = rw.read
	, write = rw.write
	, backup = rw.backup;

var XLSX = require('xlsx');

var extractFromExcel = function(excelPath, config, path) {

	var data = {};

	if(fileExists(path)) {

		console.log('Target file exists, properties will be merged');

		backup(path);

		data = read(path);
	}

	/*

	Config sample :

		{
			"sheet": "Sheet 1",
			"keyColumn": "I",
			"valueColumn": "H",
			"firstLine": 2
		}
	*/

	var defaultConfig = {

		escape: false
	};

	var actualConfig = Object.assign({}, defaultConfig, config);

	var filter = actualConfig.filter;

	if(filter) {
		var c = filter.column;
		if(!c || c === null || c === "") {
			throw "'column' field missing for filter";
		}
		var v = filter.value;
		if(!v || v === null || v === "") {
			throw "'value' field missing for filter";
		}
	}

	var keyCol = actualConfig.keyColumn,
	 	valueCol = actualConfig.valueColumn,
	 	start = actualConfig.firstLine;

	var workbook = XLSX.readFile(excelPath);

	var worksheet = workbook.Sheets[actualConfig.sheet];

	if(!worksheet || worksheet == null) {
		worksheet = workbook.Sheets[workbook.SheetNames[0]];
	}

	var range = XLSX.utils.decode_range(worksheet['!ref']);

	var end = range.e.r + 1;

	console.log(range);

	for (var i = start; i <= end; i++) {

		var propKeyAddress = keyCol + i;

		if(!worksheet.hasOwnProperty(propKeyAddress )) {
			console.log('Key missing at cell ' + propKeyAddress);
			continue;
		}

		var propKey = worksheet[propKeyAddress].v;

		var propValueAddress = valueCol + i;

		if(!worksheet.hasOwnProperty(propValueAddress)) {
			console.log('Value missing at cell ' + propValueAddress + ' (for key ' + propKey  + ')');
			continue;
		}

		if(filter) {
			var column = filter.column;
			var expectedValue = filter.value;
			var actualValueCell = worksheet[column + i];
			if(!actualValueCell || actualValueCell.v !== expectedValue) {
				continue;
			}
		}

		var propValueCell = worksheet[propValueAddress];

		if(!propValueCell) {
			continue;
		}

		var propValue = propValueCell.v;

		if(propKey && propKey !== "" && propValue && propValue !== "" ) {

			if(!actualConfig.escape) {
				propValue = propValue.replace(/[\r\n\t]/g,'');
				propValue = eval('"' + propValue.replace(/"/g,'\\"') + '"');
			}

			data[propKey] = propValue;
		}
	};

	write(path, data);
};

exports.extractFromExcel = extractFromExcel;<commit_msg>Clean console.log()<commit_after>'use strict';

var fileExists = require('file-exists');

var fs = require('fs');

var rw = require('./rw')
	, read = rw.read
	, write = rw.write
	, backup = rw.backup;

var XLSX = require('xlsx');

var extractFromExcel = function(excelPath, config, path) {

	var data = {};

	if(fileExists(path)) {

		console.log('Target file exists, properties will be merged');

		backup(path);

		data = read(path);
	}

	/*

	Config sample :

		{
			"sheet": "Sheet 1",
			"keyColumn": "I",
			"valueColumn": "H",
			"firstLine": 2
		}
	*/

	var defaultConfig = {

		escape: false
	};

	var actualConfig = Object.assign({}, defaultConfig, config);

	var filter = actualConfig.filter;

	if(filter) {
		var c = filter.column;
		if(!c || c === null || c === "") {
			throw "'column' field missing for filter";
		}
		var v = filter.value;
		if(!v || v === null || v === "") {
			throw "'value' field missing for filter";
		}
	}

	var keyCol = actualConfig.keyColumn,
	 	valueCol = actualConfig.valueColumn,
	 	start = actualConfig.firstLine;

	var workbook = XLSX.readFile(excelPath);

	var worksheet = workbook.Sheets[actualConfig.sheet];

	if(!worksheet || worksheet == null) {
		worksheet = workbook.Sheets[workbook.SheetNames[0]];
	}

	var range = XLSX.utils.decode_range(worksheet['!ref']);

	var end = range.e.r + 1;

	for (var i = start; i <= end; i++) {

		var propKeyAddress = keyCol + i;

		if(!worksheet.hasOwnProperty(propKeyAddress )) {
			console.log('Key missing at cell ' + propKeyAddress);
			continue;
		}

		var propKey = worksheet[propKeyAddress].v;

		var propValueAddress = valueCol + i;

		if(!worksheet.hasOwnProperty(propValueAddress)) {
			console.log('Value missing at cell ' + propValueAddress + ' (for key ' + propKey  + ')');
			continue;
		}

		if(filter) {
			var column = filter.column;
			var expectedValue = filter.value;
			var actualValueCell = worksheet[column + i];
			if(!actualValueCell || actualValueCell.v !== expectedValue) {
				continue;
			}
		}

		var propValueCell = worksheet[propValueAddress];

		if(!propValueCell) {
			continue;
		}

		var propValue = propValueCell.v;

		if(propKey && propKey !== "" && propValue && propValue !== "" ) {

			if(!actualConfig.escape) {
				propValue = propValue.replace(/[\r\n\t]/g,'');
				propValue = eval('"' + propValue.replace(/"/g,'\\"') + '"');
			}

			data[propKey] = propValue;
		}
	};

	write(path, data);
};

exports.extractFromExcel = extractFromExcel;<|endoftext|>


<commit_before>"use strict";

var fs          = require('fs'),
    xml2js      = require('xml2js'),
    _           = require('underscore'),
    SvgParser   = require(__dirname + '/parser'),
    builder     = require('xmlbuilder');

var Svg = function(){};

Svg.prototype.elements = [];

Svg.prototype.setElements = function(elements){
    this.elements = elements;
};

Svg.prototype.addElement = function(element){
    this.elements.push(element);
};

Svg.prototype.toJSON = function(matrix){
    var json = {
        elements : []
    };

    _.each(this.elements, function(element){
        json.elements.push(element.toJSON());
    });

    return json;
};

Svg.prototype.toXml = function(matrix){
    var xml = builder.create('svg');
    xml.att('version', '1.1');
    xml.att('xmlns', 'http://www.w3.org/2000/svg');

    _.each(this.elements, function(element){
        xml.children.push(element.toXml());
    });

    return xml;
};

Svg.prototype.toString = function(matrix){
    return this.toXml(matrix).toString();
};

module.exports = Svg;

module.exports.fromSvgDocument = function(path, callback){
    fs.readFile(path, function(error, data){
        if(error){
            callback(error);
            return;
        }
        Svg.fromString(data.toString(), callback);
    });
};

module.exports.fromString = function(string, callback){
    var parser  = new xml2js.Parser(),
        self    = this;

    parser.addListener('end', function(result) {
        SvgParser.convert(result, function(err, elements){
            if(err){
                callback(err);
                return;
            }

            var svg = new Svg();
            svg.setElements(elements);
            callback(null, svg);
        });
    });

    parser.parseString(string);
};<commit_msg>+ add jsdoc<commit_after>"use strict";

var fs          = require('fs'),
    xml2js      = require('xml2js'),
    _           = require('underscore'),
    SvgParser   = require(__dirname + '/parser'),
    builder     = require('xmlbuilder');

var Svg = function(){};

Svg.prototype.elements = [];

/**
 * Set Svg Elements
 * @param {Array}       elements            SvgObject Array (rect|polygon|polyline|...)
 */
Svg.prototype.setElements = function(elements){
    this.elements = elements;
};

/**
 * Add SvgObject element to current SVG
 * @param {SvgObject}   element             SvgObject Element
 */
Svg.prototype.addElement = function(element){
    this.elements.push(element);
};

/**
 * Convert Svg to Json format
 * @param {boolean}     matrix              Applies Matrix and convert rect to polygon and circle to ellipse
 * @returns {object}                        Svg Json Object representation
 */
Svg.prototype.toJSON = function(matrix){
    var json = {
        elements : []
    };

    _.each(this.elements, function(element){
        json.elements.push(element.toJSON());
    });

    return json;
};

/**
 * Convert Svg to Xml format
 * @param {boolean}     matrix              Applies Matrix and convert rect to polygon and circle to ellipse
 * @returns {object}                        XMLBuilder Svg representation
 */
Svg.prototype.toXml = function(matrix){
    var xml = builder.create('svg');
    xml.att('version', '1.1');
    xml.att('xmlns', 'http://www.w3.org/2000/svg');

    _.each(this.elements, function(element){
        xml.children.push(element.toXml());
    });

    return xml;
};

/**
 * Convert SVG to String :
 *     '<svg>...</svg>'
 * @param {boolean}     matrix              Applies Matrix and convert rect to polygon and circle to ellipse
 * @returns {string}                        Svg String representation
 */
Svg.prototype.toString = function(matrix){
    return this.toXml(matrix).toString();
};

module.exports = Svg;

/**
 * Create Svg from Svg Document
 * @param {string}      path                Uri of source file
 * @param {function}    callback            Callback Function
 */
module.exports.fromSvgDocument = function(path, callback){
    fs.readFile(path, function(error, data){
        if(error){
            callback(error);
            return;
        }
        Svg.fromString(data.toString(), callback);
    });
};

/**
 * Create Svg from Xml String representation
 * @param {string}      string              Svg string representation
 * @param {function}    callback            Callback Function
 */
module.exports.fromString = function(string, callback){
    var parser  = new xml2js.Parser(),
        self    = this;

    parser.addListener('end', function(result) {
        SvgParser.convert(result, function(err, elements){
            if(err){
                callback(err);
                return;
            }

            var svg = new Svg();
            svg.setElements(elements);
            callback(null, svg);
        });
    });

    parser.parseString(string);
};<|endoftext|>


<commit_before>// classList polyfill for old browsers
require('classlist-polyfill')
// Object.assign polyfill
require('es6-object-assign').polyfill()

// String to DOM function
var domify = require('domify')

// Use the DOM's HTML parsing to escape any dangerous strings
var escapeHtml = function escapeHtml (str) {
  if (typeof str !== 'undefined') {
    var div = document.createElement('div')
    div.appendChild(document.createTextNode(str))
    return div.innerHTML
  } else {
    return ''
  }
}

// Detect CSS Animation End Support
// https://github.com/limonte/sweetalert2/blob/99bd539f85e15ac170f69d35001d12e092ef0054/src/utils/dom.js#L194
var animationEndEvent = (function detectAnimationEndEvent () {
  var el = document.createElement('div')
  var eventNames = {
    'WebkitAnimation': 'webkitAnimationEnd',
    'MozAnimation': 'animationend',
    'OAnimation': 'oanimationend',
    'msAnimation': 'MSAnimationEnd',
    'animation': 'animationend'
  }
  for (var i in eventNames) {
    if (el.style[i] !== undefined) {
      return eventNames[i]
    }
  }
  return false
})()

// vex base CSS classes
var baseClassNames = {
  vex: 'vex',
  content: 'vex-content',
  overlay: 'vex-overlay',
  close: 'vex-close',
  closing: 'vex-closing',
  open: 'vex-open'
}

// Private lookup table of all open vex objects, keyed by id
var vexes = {}
var globalId = 1

// vex itself is an object that exposes a simple API to open and close vex objects in various ways
var vex = {
  open: function open (opts) {
    // Check for usage of deprecated options, and log a warning
    var warnDeprecated = function warnDeprecated (prop) {
      console.warn('The "' + prop + '" property is deprecated in vex 3. Use CSS classes and the appropriate "ClassName" options, instead.')
      console.warn('See http://github.hubspot.com/vex/api/advanced/#options')
    }
    if (opts.css) {
      warnDeprecated('css')
    }
    if (opts.overlayCSS) {
      warnDeprecated('overlayCSS')
    }
    if (opts.contentCSS) {
      warnDeprecated('contentCSS')
    }
    if (opts.closeCSS) {
      warnDeprecated('closeCSS')
    }

    // The dialog instance
    var vexInstance = {}

    // Set id
    vexInstance.id = globalId++

    // Store internally
    vexes[vexInstance.id] = vexInstance

    // Set state
    vexInstance.isOpen = true

    // Close function on the vex instance
    // This is how all API functions should close individual vexes
    vexInstance.close = function instanceClose () {
      // Check state
      if (!this.isOpen) {
        return true
      }

      var options = this.options

      // Allow the user to validate any info or abort the close with the beforeClose callback
      var shouldClose = (function shouldClose () {
        // Call before close callback
        if (options.beforeClose) {
          return options.beforeClose.call(this)
        }
        // Otherwise indicate that it's ok to continue with close
        return true
      }.bind(this)())

      // If beforeClose() fails, abort the close
      if (shouldClose === false) {
        return false
      }

      // Update state
      this.isOpen = false

      // Detect if the content el has any CSS animations defined
      var style = window.getComputedStyle(this.contentEl)
      function hasAnimationPre (prefix) {
        return style.getPropertyValue(prefix + 'animation-name') !== 'none' && style.getPropertyValue(prefix + 'animation-duration') !== '0s'
      }
      var hasAnimation = hasAnimationPre('') || hasAnimationPre('-webkit-') || hasAnimationPre('-moz-') || hasAnimationPre('-o-')

      // Define the function that will actually close the instance
      var close = function close () {
        if (!this.rootEl.parentNode) {
          return
        }
        // Run once
        this.rootEl.removeEventListener(animationEndEvent, close)
        // Remove from lookup table (prevent memory leaks)
        delete vexes[this.id]
        // Remove the dialog from the DOM
        this.rootEl.parentNode.removeChild(this.rootEl)
        // Call after close callback
        if (options.afterClose) {
          options.afterClose.call(this)
        }
        // Remove styling from the body, if no more vexes are open
        if (Object.keys(vexes).length === 0) {
          document.body.classList.remove(baseClassNames.open)
        }
      }.bind(this)

      // Close the vex
      if (animationEndEvent && hasAnimation) {
        // Setup the end event listener, to remove the el from the DOM
        this.rootEl.addEventListener(animationEndEvent, close)
        // Add the closing class to the dialog, showing the close animation
        this.rootEl.classList.add(baseClassNames.closing)
      } else {
        close()
      }

      return true
    }

    // Allow strings as content
    if (typeof opts === 'string') {
      opts = {
        content: opts
      }
    }

    // `content` is unsafe internally, so translate
    // safe default: HTML-escape the content before passing it through
    if (opts.unsafeContent && !opts.content) {
      opts.content = opts.unsafeContent
    } else if (opts.content) {
      opts.content = escapeHtml(opts.content)
    }

    // Store options on instance for future reference
    var options = vexInstance.options = Object.assign({}, vex.defaultOptions, opts)

    // vex root
    var rootEl = vexInstance.rootEl = document.createElement('div')
    rootEl.classList.add(baseClassNames.vex)
    if (options.className) {
      rootEl.classList.add(options.className)
    }

    // Overlay
    var overlayEl = vexInstance.overlayEl = document.createElement('div')
    overlayEl.classList.add(baseClassNames.overlay)
    if (options.overlayClassName) {
      overlayEl.classList.add(options.overlayClassName)
    }
    if (options.overlayClosesOnClick) {
      overlayEl.addEventListener('click', function overlayClickListener (e) {
        if (e.target === overlayEl) {
          vexInstance.close()
        }
      })
    }
    rootEl.appendChild(overlayEl)

    // Content
    var contentEl = vexInstance.contentEl = document.createElement('div')
    contentEl.classList.add(baseClassNames.content)
    if (options.contentClassName) {
      contentEl.classList.add(options.contentClassName)
    }
    contentEl.appendChild(options.content instanceof window.Node ? options.content : domify(options.content))
    rootEl.appendChild(contentEl)

    // Close button
    if (options.showCloseButton) {
      var closeEl = vexInstance.closeEl = document.createElement('div')
      closeEl.classList.add(baseClassNames.close)
      if (options.closeClassName) {
        closeEl.classList.add(options.closeClassName)
      }
      closeEl.addEventListener('click', vexInstance.close.bind(vexInstance))
      contentEl.appendChild(closeEl)
    }

    // Add to DOM
    document.querySelector(options.appendLocation).appendChild(rootEl)

    // Call after open callback
    if (options.afterOpen) {
      options.afterOpen.call(vexInstance)
    }

    // Apply styling to the body
    document.body.classList.add(baseClassNames.open)

    // Return the created vex instance
    return vexInstance
  },

  // A top-level vex.close function to close dialogs by reference or id
  close: function close (vexOrId) {
    var id
    if (vexOrId.id) {
      id = vexOrId.id
    } else if (typeof vexOrId === 'string') {
      id = vexOrId
    } else {
      throw new TypeError('close requires a vex object or id string')
    }
    if (!vexes[id]) {
      return false
    }
    return vexes[id].close()
  },

  // Close the most recently created/opened vex
  closeTop: function closeTop () {
    var ids = Object.keys(vexes)
    if (!ids.length) {
      return false
    }
    return vexes[ids[ids.length - 1]].close()
  },

  // Close every vex!
  closeAll: function closeAll () {
    for (var id in vexes) {
      this.close(id)
    }
    return true
  },

  // A getter for the internal lookup table
  getAll: function getAll () {
    return vexes
  },

  // A getter for the internal lookup table
  getById: function getById (id) {
    return vexes[id]
  }
}

// Close top vex on escape
window.addEventListener('keyup', function vexKeyupListener (e) {
  if (e.keyCode === 27) {
    vex.closeTop()
  }
})
// Close all vexes on history pop state (useful in single page apps)
window.addEventListener('popstate', vex.closeAll)

vex.defaultOptions = {
  content: '',
  showCloseButton: true,
  escapeButtonCloses: true,
  overlayClosesOnClick: true,
  appendLocation: 'body',
  className: '',
  overlayClassName: '',
  contentClassName: '',
  closeClassName: ''
}

// TODO Loading symbols?

// Plugin system!
vex.registerPlugin = function registerPlugin (pluginFn, name) {
  var plugin = pluginFn(vex)
  var pluginName = name || plugin.name
  if (vex[pluginName]) {
    throw new Error('Plugin ' + name + ' is already registered.')
  }
  vex[pluginName] = plugin
}

module.exports = vex
<commit_msg>Add _escapeHtml prop to library object<commit_after>// classList polyfill for old browsers
require('classlist-polyfill')
// Object.assign polyfill
require('es6-object-assign').polyfill()

// String to DOM function
var domify = require('domify')

// Use the DOM's HTML parsing to escape any dangerous strings
var escapeHtml = function escapeHtml (str) {
  if (typeof str !== 'undefined') {
    var div = document.createElement('div')
    div.appendChild(document.createTextNode(str))
    return div.innerHTML
  } else {
    return ''
  }
}

// Detect CSS Animation End Support
// https://github.com/limonte/sweetalert2/blob/99bd539f85e15ac170f69d35001d12e092ef0054/src/utils/dom.js#L194
var animationEndEvent = (function detectAnimationEndEvent () {
  var el = document.createElement('div')
  var eventNames = {
    'WebkitAnimation': 'webkitAnimationEnd',
    'MozAnimation': 'animationend',
    'OAnimation': 'oanimationend',
    'msAnimation': 'MSAnimationEnd',
    'animation': 'animationend'
  }
  for (var i in eventNames) {
    if (el.style[i] !== undefined) {
      return eventNames[i]
    }
  }
  return false
})()

// vex base CSS classes
var baseClassNames = {
  vex: 'vex',
  content: 'vex-content',
  overlay: 'vex-overlay',
  close: 'vex-close',
  closing: 'vex-closing',
  open: 'vex-open'
}

// Private lookup table of all open vex objects, keyed by id
var vexes = {}
var globalId = 1

// vex itself is an object that exposes a simple API to open and close vex objects in various ways
var vex = {
  open: function open (opts) {
    // Check for usage of deprecated options, and log a warning
    var warnDeprecated = function warnDeprecated (prop) {
      console.warn('The "' + prop + '" property is deprecated in vex 3. Use CSS classes and the appropriate "ClassName" options, instead.')
      console.warn('See http://github.hubspot.com/vex/api/advanced/#options')
    }
    if (opts.css) {
      warnDeprecated('css')
    }
    if (opts.overlayCSS) {
      warnDeprecated('overlayCSS')
    }
    if (opts.contentCSS) {
      warnDeprecated('contentCSS')
    }
    if (opts.closeCSS) {
      warnDeprecated('closeCSS')
    }

    // The dialog instance
    var vexInstance = {}

    // Set id
    vexInstance.id = globalId++

    // Store internally
    vexes[vexInstance.id] = vexInstance

    // Set state
    vexInstance.isOpen = true

    // Close function on the vex instance
    // This is how all API functions should close individual vexes
    vexInstance.close = function instanceClose () {
      // Check state
      if (!this.isOpen) {
        return true
      }

      var options = this.options

      // Allow the user to validate any info or abort the close with the beforeClose callback
      var shouldClose = (function shouldClose () {
        // Call before close callback
        if (options.beforeClose) {
          return options.beforeClose.call(this)
        }
        // Otherwise indicate that it's ok to continue with close
        return true
      }.bind(this)())

      // If beforeClose() fails, abort the close
      if (shouldClose === false) {
        return false
      }

      // Update state
      this.isOpen = false

      // Detect if the content el has any CSS animations defined
      var style = window.getComputedStyle(this.contentEl)
      function hasAnimationPre (prefix) {
        return style.getPropertyValue(prefix + 'animation-name') !== 'none' && style.getPropertyValue(prefix + 'animation-duration') !== '0s'
      }
      var hasAnimation = hasAnimationPre('') || hasAnimationPre('-webkit-') || hasAnimationPre('-moz-') || hasAnimationPre('-o-')

      // Define the function that will actually close the instance
      var close = function close () {
        if (!this.rootEl.parentNode) {
          return
        }
        // Run once
        this.rootEl.removeEventListener(animationEndEvent, close)
        // Remove from lookup table (prevent memory leaks)
        delete vexes[this.id]
        // Remove the dialog from the DOM
        this.rootEl.parentNode.removeChild(this.rootEl)
        // Call after close callback
        if (options.afterClose) {
          options.afterClose.call(this)
        }
        // Remove styling from the body, if no more vexes are open
        if (Object.keys(vexes).length === 0) {
          document.body.classList.remove(baseClassNames.open)
        }
      }.bind(this)

      // Close the vex
      if (animationEndEvent && hasAnimation) {
        // Setup the end event listener, to remove the el from the DOM
        this.rootEl.addEventListener(animationEndEvent, close)
        // Add the closing class to the dialog, showing the close animation
        this.rootEl.classList.add(baseClassNames.closing)
      } else {
        close()
      }

      return true
    }

    // Allow strings as content
    if (typeof opts === 'string') {
      opts = {
        content: opts
      }
    }

    // `content` is unsafe internally, so translate
    // safe default: HTML-escape the content before passing it through
    if (opts.unsafeContent && !opts.content) {
      opts.content = opts.unsafeContent
    } else if (opts.content) {
      opts.content = escapeHtml(opts.content)
    }

    // Store options on instance for future reference
    var options = vexInstance.options = Object.assign({}, vex.defaultOptions, opts)

    // vex root
    var rootEl = vexInstance.rootEl = document.createElement('div')
    rootEl.classList.add(baseClassNames.vex)
    if (options.className) {
      rootEl.classList.add(options.className)
    }

    // Overlay
    var overlayEl = vexInstance.overlayEl = document.createElement('div')
    overlayEl.classList.add(baseClassNames.overlay)
    if (options.overlayClassName) {
      overlayEl.classList.add(options.overlayClassName)
    }
    if (options.overlayClosesOnClick) {
      overlayEl.addEventListener('click', function overlayClickListener (e) {
        if (e.target === overlayEl) {
          vexInstance.close()
        }
      })
    }
    rootEl.appendChild(overlayEl)

    // Content
    var contentEl = vexInstance.contentEl = document.createElement('div')
    contentEl.classList.add(baseClassNames.content)
    if (options.contentClassName) {
      contentEl.classList.add(options.contentClassName)
    }
    contentEl.appendChild(options.content instanceof window.Node ? options.content : domify(options.content))
    rootEl.appendChild(contentEl)

    // Close button
    if (options.showCloseButton) {
      var closeEl = vexInstance.closeEl = document.createElement('div')
      closeEl.classList.add(baseClassNames.close)
      if (options.closeClassName) {
        closeEl.classList.add(options.closeClassName)
      }
      closeEl.addEventListener('click', vexInstance.close.bind(vexInstance))
      contentEl.appendChild(closeEl)
    }

    // Add to DOM
    document.querySelector(options.appendLocation).appendChild(rootEl)

    // Call after open callback
    if (options.afterOpen) {
      options.afterOpen.call(vexInstance)
    }

    // Apply styling to the body
    document.body.classList.add(baseClassNames.open)

    // Return the created vex instance
    return vexInstance
  },

  // A top-level vex.close function to close dialogs by reference or id
  close: function close (vexOrId) {
    var id
    if (vexOrId.id) {
      id = vexOrId.id
    } else if (typeof vexOrId === 'string') {
      id = vexOrId
    } else {
      throw new TypeError('close requires a vex object or id string')
    }
    if (!vexes[id]) {
      return false
    }
    return vexes[id].close()
  },

  // Close the most recently created/opened vex
  closeTop: function closeTop () {
    var ids = Object.keys(vexes)
    if (!ids.length) {
      return false
    }
    return vexes[ids[ids.length - 1]].close()
  },

  // Close every vex!
  closeAll: function closeAll () {
    for (var id in vexes) {
      this.close(id)
    }
    return true
  },

  // A getter for the internal lookup table
  getAll: function getAll () {
    return vexes
  },

  // A getter for the internal lookup table
  getById: function getById (id) {
    return vexes[id]
  }
}

// Close top vex on escape
window.addEventListener('keyup', function vexKeyupListener (e) {
  if (e.keyCode === 27) {
    vex.closeTop()
  }
})
// Close all vexes on history pop state (useful in single page apps)
window.addEventListener('popstate', vex.closeAll)

vex.defaultOptions = {
  content: '',
  showCloseButton: true,
  escapeButtonCloses: true,
  overlayClosesOnClick: true,
  appendLocation: 'body',
  className: '',
  overlayClassName: '',
  contentClassName: '',
  closeClassName: ''
}

// TODO Loading symbols?

// Include escapeHtml function on the library object
Object.defineProperty(vex, '_escapeHtml', {
  configurable: false,
  enumerable: false,
  writable: false,
  value: escapeHtml
})

// Plugin system!
vex.registerPlugin = function registerPlugin (pluginFn, name) {
  var plugin = pluginFn(vex)
  var pluginName = name || plugin.name
  if (vex[pluginName]) {
    throw new Error('Plugin ' + name + ' is already registered.')
  }
  vex[pluginName] = plugin
}

module.exports = vex
<|endoftext|>


<commit_before>var _ = require('lodash');
var listToArray = require('list-to-array');

/**
 * Applies option field transforms to get the CSV value for a field
 */

function transformFieldValue (field, item) {
	var transform = typeof field.options.toCSV === 'string'
		? listToArray(field.options.toCSV)
		: field.options.toCSV;

	if (typeof transform === 'function') {
		return transform.call(item, field);
	}

	if (Array.isArray(transform)) {
		var value = item.get(field.path);
		if (transform.length === 1) {
			return value[transform[0]];
		} else {
			return _.pick(value, transform);
		}
	}

	return field.format(item);
}

/**
 * Gets the data from an Item ready to be serialised to CSV for download
 */

function getCSVData (item, options) {
	if (!options) {
		options = {};
	}
	var fields = options.fields;
	if (fields === undefined) {
		fields = Object.keys(this.fields);
	}
	var data = {
		id: String(item.id),
	};
	if (this.autokey) {
		data[this.autokey.path] = item.get(this.autokey.path);
	}
	if (fields) {
		if (typeof fields === 'string') {
			fields = listToArray(fields);
		}
		if (!Array.isArray(fields)) {
			throw new Error('List.getCSV: fields must be undefined, a string, or an array.');
		}
		fields.forEach(function (path) {
			var field = this.fields[path];
			if (field) {
				if (field.type === 'relationship' && options.expandRelationshipFields) {
					var expanded = field.getExpandedData(item);
					if (Array.isArray(expanded)) {
						data[path] = expanded.map(function (i) {
							return i.name ? i.name + ' (' + i.id + ')' : i.id;
						}).join(',');
					} else if (typeof expanded === 'object') {
						data[path] = expanded.name;
						data[path + 'Id'] = expanded.id;
					} else {
						data[path] = '';
					}
				} else {
					data[path] = transformFieldValue(field, item);
				}
			} else {
				data[path] = item.get(path);
			}
		}, this);
	}
	// Copy each value into the return structure, flattening arrays into lists and
	// flattening objects into a column per property (one level only)
	var rtn = {};
	_.forOwn(data, function (value, prop) {
		if (Array.isArray(value)) {
			// Array values are serialised to JSON, this should be an edge-case catch
			// for data coming raw from the item; array-type fields will already have
			// been stringified by the field.format method.
			rtn[prop] = JSON.stringify(value);
		} else if (typeof value === 'object') {
			// For object values, we loop through each key and add it to its own column
			// in the csv. Complex values are serialised to JSON.
			_.forOwn(value, function (v, i) {
				var suffix = i.substr(0, 1).toUpperCase() + i.substr(1);
				rtn[prop + suffix] = (typeof v === 'object') ? JSON.stringify(v) : v;
			});
		} else {
			rtn[prop] = value;
		}
	});

	return rtn;
}

module.exports = getCSVData;
<commit_msg>getCSVData: using options.fields instead of local fields variable<commit_after>var _ = require('lodash');
var listToArray = require('list-to-array');

/**
 * Applies option field transforms to get the CSV value for a field
 */

function transformFieldValue (field, item) {
	var transform = typeof field.options.toCSV === 'string'
		? listToArray(field.options.toCSV)
		: field.options.toCSV;
	if (typeof transform === 'function') {
		return transform.call(item, field);
	}
	if (Array.isArray(transform)) {
		var value = item.get(field.path);
		if (transform.length === 1) {
			return value[transform[0]];
		} else {
			return _.pick(value, transform);
		}
	}
	return field.format(item);
}

/**
 * Gets the data from an Item ready to be serialised to CSV for download
 */

function getCSVData (item, options) {
	if (!options) {
		options = {};
	}
	options.fields;
	if (options.fields === undefined) {
		options.fields = Object.keys(this.options.fields);
	}
	var data = {
		id: String(item.id),
	};
	if (this.autokey) {
		data[this.autokey.path] = item.get(this.autokey.path);
	}
	if (options.fields) {
		if (typeof options.fields === 'string') {
			options.fields = listToArray(options.fields);
		}
		if (!Array.isArray(options.fields)) {
			throw new Error('List.getCSV: options.fields must be undefined, a string, or an array.');
		}
		options.fields.forEach(function (path) {
			var field = this.fields[path];
			if (field) {
				if (field.type === 'relationship' && options.expandRelationshipFields) {
					var expanded = field.getExpandedData(item);
					if (Array.isArray(expanded)) {
						data[path] = expanded.map(function (i) {
							return i.name ? i.name + ' (' + i.id + ')' : i.id;
						}).join(',');
					} else if (typeof expanded === 'object') {
						data[path] = expanded.name;
						data[path + 'Id'] = expanded.id;
					} else {
						data[path] = '';
					}
				} else {
					data[path] = transformFieldValue(field, item);
				}
			} else {
				data[path] = item.get(path);
			}
		}, this);
	}
	// Copy each value into the return structure, flattening arrays into lists and
	// flattening objects into a column per property (one level only)
	var rtn = {};
	_.forOwn(data, function (value, prop) {
		if (Array.isArray(value)) {
			// Array values are serialised to JSON, this should be an edge-case catch
			// for data coming raw from the item; array-type fields will already have
			// been stringified by the field.format method.
			rtn[prop] = JSON.stringify(value);
		} else if (typeof value === 'object') {
			// For object values, we loop through each key and add it to its own column
			// in the csv. Complex values are serialised to JSON.
			_.forOwn(value, function (v, i) {
				var suffix = i.substr(0, 1).toUpperCase() + i.substr(1);
				rtn[prop + suffix] = (typeof v === 'object') ? JSON.stringify(v) : v;
			});
		} else {
			rtn[prop] = value;
		}
	});

	return rtn;
}

module.exports = getCSVData;
<|endoftext|>


<commit_before>import { toKey } from '../helpers';
import gravity from '../apis/gravity';
import httpLoader from './http';
import authenticatedHttpLoader from './authenticated_http';

export const gravityLoader = httpLoader(gravity);

const load = (path, options = {}) => {
  const key = toKey(path, options);
  return gravityLoader.load(key);
};

load.with = (accessToken) => {
  const authenticatedGravityLoader = authenticatedHttpLoader(gravity, accessToken);
  return (path, options = {}) => {
    const key = toKey(path, options);
    return authenticatedGravityLoader(key, accessToken);
  };
};

export default load;
<commit_msg>Glue `all` onto the gravity loader<commit_after>import { toKey } from '../helpers';
import gravity from '../apis/gravity';
import httpLoader from './http';
import authenticatedHttpLoader from './authenticated_http';
import all from '../all';

export const gravityLoader = httpLoader(gravity);

const load = (path, options = {}) => {
  const key = toKey(path, options);
  return gravityLoader.load(key);
};

load.with = (accessToken) => {
  const authenticatedGravityLoader = authenticatedHttpLoader(gravity, accessToken);
  return (path, options = {}) => {
    const key = toKey(path, options);
    return authenticatedGravityLoader(key, accessToken);
  };
};

load.all = all;

export default load;
<|endoftext|>


<commit_before>'use strict';

var crypto = require('../crypto')();
var AuthError = require('../errors').AuthError;
var lxHelpers = require('lx-helpers');

/**
 * Auth Object
 *
 * @param {!Object} baboon
 * @returns {Object}
 */
module.exports = function (baboon) {

    var pub = {};
    var config = baboon.config;
    var rights = baboon.rights;
    var syslog = baboon.loggers.syslog;

    /**
     * Authenticate
     * Helper for login
     *
     * @param username
     * @param password
     * @param callback
     */
    var authenticate = function (username, password, callback) {

        // get user from db
        rights.getUser(username, function (error, user) {
            if(!user.is_active) {
                callback(new AuthError('Login Failed, account is inactive', 403));
            }
            else if (!error && !lxHelpers.isEmpty(user)) {
                // check password
                crypto.compare(password, user.password, user.salt, function (error, result) {
                    if (!error && result) {
                        if (result.is_equal) {
                            // delete hash and salt from user
                            delete user.password;
                            delete user.salt;

                            callback(null, user);
                        }
                        else {
                            callback(new AuthError('Login Failed, password for ' + username + ' does not match', 403));
                        }
                    }
                    else {
                        callback(error || new AuthError('Login Failed, unknown error in compare password', 500));
                    }
                });
            }
            else {
                callback(new AuthError('Login failed, ' + username + ' not found', 403));
            }
        });
    };

    /**
     * Login
     * Login with username and password
     *
     * @param req
     * @param res
     * @param next
     */
    pub.login = function (req, res, next) {

        // params
        var user = req.body.user;

        if (typeof user !== 'object') {
            return next(new AuthError('Parameter user is required and must be a object type!', 400));
        }

        if (typeof user.username !== 'string') {
            return next(new AuthError('Parameter username is required and must be a string type!', 400));
        }

        if (typeof user.password !== 'string') {
            return next(new AuthError('Parameter password is required and must be a string type!', 400));
        }

        // check guest system user
        if (user.username.toLowerCase() === 'guest') {
            return next(new AuthError('Guest is a reserved system name and can not be used for the login.', 400));
        }

        authenticate(user.username, user.password, function (error, user) {

            if (!error && user) {

                // login successfully, setup session
                req.session.regenerate(function () {

                    // new session
                    req.session.activity = new Date();
                    req.session.start = new Date();
                    req.session.data = {};
                    req.session.loggedIn = true;
                    req.session.user = user;

                    syslog.debug('session regenerate after login with user: ' + user.name);
                    return res.json(200, 'login successfully');
                });
            }
            else {
                syslog.warn(error.name + ': ' + error.message);
                return next(new AuthError('Username or password do not match', 403), res);
            }
        });
    };

    /**
     * logout
     * Logout destroy the user's session to log them out.
     * A new session will be re-created next request.
     *
     * @param req
     * @param res
     */
    pub.logout = function (req, res) {
        req.session.destroy(function () {
            res.redirect('/');
        });
    };

    /**
     * restricted
     *
     * @param {!Object} req
     * @param {!Object} res
     * @param {!Function} next
     */
    pub.restricted = function (req, res, next) {

        // check session and user when rights is enabled and masterLoginPage is true
        if (config.rights.enabled && config.rights.masterLoginPage) {

            // not in Guest role
            if (req.session.user && !rights.userIsInRole(req.session.user, 'Guest')) {
                next();
            }
            else {
                res.redirect('/account/login');
            }
        }
        else {
            next();
        }
    };

    /**
     * restrictedUser
     *
     * @param {!Object} req
     * @param {!Object} res
     * @param {!Function} next
     */
    pub.restrictedUser = function (req, res, next) {

        // check session and user when rights is enabled and masterLoginPage is true
        if (config.rights.enabled && config.rights.masterLoginPage) {

            // in User or Admin role
            if (req.session.user && rights.userIsInRole(req.session.user, 'User') ||
                rights.userIsInRole(req.session.user, 'Admin')) {
                next();
            }
            else {
                res.redirect('/account/login');
            }
        }
        else {
            next();
        }
    };

    /**
     * restrictedAdmin
     *
     * @param {!Object} req
     * @param {!Object} res
     * @param {!Function} next
     */
    pub.restrictedAdmin = function (req, res, next) {

        // check session and user when rights is enabled and masterLoginPage is true
        if (config.rights.enabled && config.rights.masterLoginPage) {

            if (req.session.user && rights.userIsInRole(req.session.user, 'Admin')) {
                next();

            }
            else {
                res.redirect('/account/login');
            }
        }
        else {
            next();
        }
    };

    return pub;
};<commit_msg>fix: login with sysadmin now also works with masterloginpage<commit_after>'use strict';

var crypto = require('../crypto')();
var AuthError = require('../errors').AuthError;
var lxHelpers = require('lx-helpers');

/**
 * Auth Object
 *
 * @param {!Object} baboon
 * @returns {Object}
 */
module.exports = function (baboon) {

    var pub = {};
    var config = baboon.config;
    var rights = baboon.rights;
    var syslog = baboon.loggers.syslog;

    /**
     * Authenticate
     * Helper for login
     *
     * @param username
     * @param password
     * @param callback
     */
    var authenticate = function (username, password, callback) {

        // get user from db
        rights.getUser(username, function (error, user) {
            if(!user.is_active) {
                callback(new AuthError('Login Failed, account is inactive', 403));
            }
            else if (!error && !lxHelpers.isEmpty(user)) {
                // check password
                crypto.compare(password, user.password, user.salt, function (error, result) {
                    if (!error && result) {
                        if (result.is_equal) {
                            // delete hash and salt from user
                            delete user.password;
                            delete user.salt;

                            callback(null, user);
                        }
                        else {
                            callback(new AuthError('Login Failed, password for ' + username + ' does not match', 403));
                        }
                    }
                    else {
                        callback(error || new AuthError('Login Failed, unknown error in compare password', 500));
                    }
                });
            }
            else {
                callback(new AuthError('Login failed, ' + username + ' not found', 403));
            }
        });
    };

    /**
     * Login
     * Login with username and password
     *
     * @param req
     * @param res
     * @param next
     */
    pub.login = function (req, res, next) {

        // params
        var user = req.body.user;

        if (typeof user !== 'object') {
            return next(new AuthError('Parameter user is required and must be a object type!', 400));
        }

        if (typeof user.username !== 'string') {
            return next(new AuthError('Parameter username is required and must be a string type!', 400));
        }

        if (typeof user.password !== 'string') {
            return next(new AuthError('Parameter password is required and must be a string type!', 400));
        }

        // check guest system user
        if (user.username.toLowerCase() === 'guest') {
            return next(new AuthError('Guest is a reserved system name and can not be used for the login.', 400));
        }

        authenticate(user.username, user.password, function (error, user) {

            if (!error && user) {

                // login successfully, setup session
                req.session.regenerate(function () {

                    // new session
                    req.session.activity = new Date();
                    req.session.start = new Date();
                    req.session.data = {};
                    req.session.loggedIn = true;
                    req.session.user = user;

                    syslog.debug('session regenerate after login with user: ' + user.name);
                    return res.json(200, 'login successfully');
                });
            }
            else {
                syslog.warn(error.name + ': ' + error.message);
                return next(new AuthError('Username or password do not match', 403), res);
            }
        });
    };

    /**
     * logout
     * Logout destroy the user's session to log them out.
     * A new session will be re-created next request.
     *
     * @param req
     * @param res
     */
    pub.logout = function (req, res) {
        req.session.destroy(function () {
            res.redirect('/');
        });
    };

    /**
     * restricted
     *
     * @param {!Object} req
     * @param {!Object} res
     * @param {!Function} next
     */
    pub.restricted = function (req, res, next) {

        // check session and user when rights is enabled and masterLoginPage is true
        if (config.rights.enabled && config.rights.masterLoginPage) {

            // not in Guest role
            if (req.session.user && !rights.userIsInRole(req.session.user, 'Guest')) {
                next();
            }
            else {
                res.redirect('/account/login');
            }
        }
        else {
            next();
        }
    };

    /**
     * restrictedUser
     *
     * @param {!Object} req
     * @param {!Object} res
     * @param {!Function} next
     */
    pub.restrictedUser = function (req, res, next) {

        // check session and user when rights is enabled and  masterLoginPage is true
        if (config.rights.enabled && config.rights.masterLoginPage) {

            // in User or Admin role
            if (req.session.user && rights.userIsInRole(req.session.user, 'User') ||
                rights.userIsInRole(req.session.user, 'Admin') || req.session.user.name === 'sysadmin') {
                next();
            }
            else {
                res.redirect('/account/login');
            }
        }
        else {
            next();
        }
    };

    /**
     * restrictedAdmin
     *
     * @param {!Object} req
     * @param {!Object} res
     * @param {!Function} next
     */
    pub.restrictedAdmin = function (req, res, next) {

        // check session and user when rights is enabled and masterLoginPage is true
        if (config.rights.enabled && config.rights.masterLoginPage) {

            if (req.session.user && rights.userIsInRole(req.session.user, 'Admin')) {
                next();

            }
            else {
                res.redirect('/account/login');
            }
        }
        else {
            next();
        }
    };

    return pub;
};<|endoftext|>


<commit_before>/**
 *      Multihost server
 *
 *      Master multihost functionality
 *
 *      Copyright 2014-2020 bluefox <dogafox@gmail.com>,
 *      MIT License
 *
 */

'use strict';
const dgram          = require('dgram');
const tools          = require('./tools');
const PORT           = 50005;
const MULTICAST_ADDR = '239.255.255.250';

/** @class */
function MHServer(hostname, logger, config, info, ips, secret) {
    const count     = 0;
    const buffer    = {};
    const lastFrame = {};
    const authList  = {};

    let server      = null;
    let initTimer   = null;
    let stopped     = false;
    let crypto;

    config = Object.assign({}, config); // make a copy

    if (config.objects) {
        config.objects = {
            type:     config.objects.type,
            host:     config.objects.host,
            port:     config.objects.port,
            user:     config.objects.user,
            pass:     config.objects.pass,
            options:  config.objects.options,
            maxQueue: config.objects.maxQueue
        };
    }

    if (config.states) {
        config.states = {
            type:     config.states.type,
            host:     config.states.host,
            port:     config.states.port,
            user:     config.states.user,
            pass:     config.states.pass,
            options:  config.states.options,
            maxQueue: config.states.maxQueue
        };
    }

    function send(msg, rinfo) {
        if (server) {
            setImmediate(() => {
                const text = JSON.stringify(msg);
                try {
                    server.send(text, 0, text.length, rinfo.port, rinfo.address);
                } catch (e) {
                    logger.warn(`host.${hostname} Multi-host discovery server: cannot send answer to ${rinfo.address}:${rinfo.port}: ${e}`);
                }
            });
        }
    }

    // delete all old connections
    function checkAuthList(ts) {
        ts = ts || new Date().getTime();
        for (const id of Object.keys(authList)) {
            if (!authList[id]) {
                delete authList[id];
            } else if (ts - authList[id].ts > 31000) {
                delete authList[id];
            }
        }
    }

    function sha(secret, salt, callback) {
        // calculate sha256
        crypto = crypto || require('crypto');
        const hash = crypto.createHash('sha256');

        hash.on('readable', () => {
            const data = hash.read();
            data && callback(data.toString('hex'));
        });

        hash.write(secret + salt);
        hash.end();
    }

    // hello => auth => browse
    function process(msg, rinfo) {
        if (!msg) {
            return;
        }

        const ts = new Date().getTime();
        checkAuthList(ts);

        const id = rinfo.address + ':' + rinfo.port;

        switch (msg.cmd) {
            case 'browse':
                if (secret && msg.password && authList[id]) {
                    return sha(secret, authList[id].salt, shaText => {
                        if (shaText !== msg.password) {
                            send({
                                auth:   config.multihostService.secure,
                                cmd:    msg.cmd,
                                id:     msg.id,
                                result: 'invalid password'
                            }, rinfo);
                        } else {
                            authList[id].auth = true;
                            send({
                                auth:     config.multihostService.secure,
                                cmd:      msg.cmd,
                                id:       msg.id,
                                objects:  config.objects,
                                states:   config.states,
                                info:     info,
                                hostname: hostname,
                                slave:    !tools.isLocalObjectsDbServer(config.objects.type, config.objects.host),
                                result:   'ok'
                            }, rinfo);
                        }
                    });
                }

                if (!config.multihostService.secure || (authList[id] && authList[id].auth)) {
                    send({
                        auth:     config.multihostService.secure,
                        cmd:      msg.cmd,
                        id:       msg.id,
                        objects:  config.objects,
                        states:   config.states,
                        info:     info,
                        hostname: hostname,
                        slave:    !tools.isLocalObjectsDbServer(config.objects.type, config.objects.host),
                        result:   'ok'
                    }, rinfo);
                } else {
                    authList[id] = {
                        time: ts,
                        salt: (Math.random() * 1000000 + ts).toString().substring(0, 16),
                        auth: false
                    };
                    // padding
                    if (authList[id].salt.length < 16) {
                        authList[id].salt += new Array(16 - authList[id].salt.length).join('_');
                    }
                    send({
                        auth:   config.multihostService.secure,
                        cmd:    msg.cmd,
                        id:     msg.id,
                        result: 'not authenticated',
                        salt:   authList[id].salt
                    }, rinfo);
                }
                break;

            default:
                send({
                    cmd:     msg.cmd,
                    id:      msg.id,
                    result:  'unknown command'
                }, rinfo);
                break;
        }
    }

    this.init = function () {
        stopped = false;
        if (initTimer) {
            clearTimeout(initTimer);
            initTimer = null;
        }

        if (count > 10) {
            return logger.warn('host.' + hostname + ' Multi-host discovery server: Port ' + PORT + ' is occupied. Service stopped.');
        }

        server = dgram.createSocket('udp4');

        server.on('error', err => {
            logger.error('host.' + hostname + ' Multi-host discovery server: error: ' + err.stack);
            server.close();
            server = null;

            initTimer = initTimer || setTimeout(() => {
                initTimer = null;
                this.init();
            }, 5000);
        });

        server.on('close', _err => {
            server = null;

            if (!initTimer && !stopped) {
                initTimer = setTimeout(() => {
                    initTimer = null;
                    this.init();
                }, 5000);
            }
        });

        server.on('message', (msg, rinfo) => {
            // following messages are allowed
            const text = msg.toString();
            const now  = new Date().getTime();
            const id = rinfo.address + ':' + rinfo.port;

            for (const ids in buffer) {
                if (!lastFrame[ids]) {
                    delete buffer[ids];
                } else if (now - lastFrame[ids] > 1000) {
                    delete buffer[ids];
                    delete lastFrame[ids];
                }
            }

            if (lastFrame[id] && now - lastFrame[id] > 1000) {
                buffer[id] = '';
            }

            lastFrame[id] = now;

            if (!buffer[id] && text[0] !== '{') {
                // ignore message
                logger.debug(`host.${hostname} Multi-host discovery server: Message from ${rinfo.address} ignored: ${text}`);
            } else {
                buffer[id] = (buffer[id] || '') + msg.toString();
                if (buffer[id] && buffer[id][buffer[id].length - 1] === '}') {
                    try {
                        const data = JSON.parse(buffer[id]);
                        buffer[id] = '';
                        if (data) {
                            process(data, rinfo);
                        }
                    } catch {
                        // may be not yet complete.
                    }
                }
            }
        });

        server.on('listening', () => {
            try {
                server.addMembership(MULTICAST_ADDR);
            } catch {
                logger.warn(`host.${hostname} Multi-host discovery server: Multicast membership could not be added.`);
            }
            const address = server.address();
            logger.info(`host.${hostname} Multi-host discovery server: service started on ${address.address}:${address.port}`);
        });

        server.bind(PORT);
    };

    this.close = function (callback) {
        stopped = true;
        if (initTimer) {
            clearTimeout(initTimer);
            initTimer = null;
        }
        if (server) {
            try {
                server.close(callback);
                server = null;
            } catch {
                server = null;
                callback && callback();
            }
        } else if (callback) {
            callback();
        }
    };

    this.init();

    return this;
}

module.exports = MHServer;
<commit_msg>* (Apollon77/oweitman) prevent errors when listening for multihost messages, fixes #1255<commit_after>/**
 *      Multihost server
 *
 *      Master multihost functionality
 *
 *      Copyright 2014-2020 bluefox <dogafox@gmail.com>,
 *      MIT License
 *
 */

'use strict';
const dgram          = require('dgram');
const tools          = require('./tools');
const PORT           = 50005;
const MULTICAST_ADDR = '239.255.255.250';

/** @class */
function MHServer(hostname, logger, config, info, ips, secret) {
    const count     = 0;
    const buffer    = {};
    const lastFrame = {};
    const authList  = {};

    let server      = null;
    let initTimer   = null;
    let stopped     = false;
    let crypto;

    config = Object.assign({}, config); // make a copy

    if (config.objects) {
        config.objects = {
            type:     config.objects.type,
            host:     config.objects.host,
            port:     config.objects.port,
            user:     config.objects.user,
            pass:     config.objects.pass,
            options:  config.objects.options,
            maxQueue: config.objects.maxQueue
        };
    }

    if (config.states) {
        config.states = {
            type:     config.states.type,
            host:     config.states.host,
            port:     config.states.port,
            user:     config.states.user,
            pass:     config.states.pass,
            options:  config.states.options,
            maxQueue: config.states.maxQueue
        };
    }

    function send(msg, rinfo) {
        if (server) {
            setImmediate(() => {
                const text = JSON.stringify(msg);
                try {
                    server.send(text, 0, text.length, rinfo.port, rinfo.address);
                } catch (e) {
                    logger.warn(`host.${hostname} Multi-host discovery server: cannot send answer to ${rinfo.address}:${rinfo.port}: ${e}`);
                }
            });
        }
    }

    // delete all old connections
    function checkAuthList(ts) {
        ts = ts || new Date().getTime();
        for (const id of Object.keys(authList)) {
            if (!authList[id]) {
                delete authList[id];
            } else if (ts - authList[id].ts > 31000) {
                delete authList[id];
            }
        }
    }

    function sha(secret, salt, callback) {
        // calculate sha256
        crypto = crypto || require('crypto');
        const hash = crypto.createHash('sha256');

        hash.on('readable', () => {
            const data = hash.read();
            data && callback(data.toString('hex'));
        });

        hash.write(secret + salt);
        hash.end();
    }

    // hello => auth => browse
    function process(msg, rinfo) {
        if (!msg) {
            return;
        }

        const ts = new Date().getTime();
        checkAuthList(ts);

        const id = rinfo.address + ':' + rinfo.port;

        switch (msg.cmd) {
            case 'browse':
                if (secret && msg.password && authList[id]) {
                    return sha(secret, authList[id].salt, shaText => {
                        if (shaText !== msg.password) {
                            send({
                                auth:   config.multihostService.secure,
                                cmd:    msg.cmd,
                                id:     msg.id,
                                result: 'invalid password'
                            }, rinfo);
                        } else {
                            authList[id].auth = true;
                            send({
                                auth:     config.multihostService.secure,
                                cmd:      msg.cmd,
                                id:       msg.id,
                                objects:  config.objects,
                                states:   config.states,
                                info:     info,
                                hostname: hostname,
                                slave:    !tools.isLocalObjectsDbServer(config.objects.type, config.objects.host),
                                result:   'ok'
                            }, rinfo);
                        }
                    });
                }

                if (!config.multihostService.secure || (authList[id] && authList[id].auth)) {
                    send({
                        auth:     config.multihostService.secure,
                        cmd:      msg.cmd,
                        id:       msg.id,
                        objects:  config.objects,
                        states:   config.states,
                        info:     info,
                        hostname: hostname,
                        slave:    !tools.isLocalObjectsDbServer(config.objects.type, config.objects.host),
                        result:   'ok'
                    }, rinfo);
                } else {
                    authList[id] = {
                        time: ts,
                        salt: (Math.random() * 1000000 + ts).toString().substring(0, 16),
                        auth: false
                    };
                    // padding
                    if (authList[id].salt.length < 16) {
                        authList[id].salt += new Array(16 - authList[id].salt.length).join('_');
                    }
                    send({
                        auth:   config.multihostService.secure,
                        cmd:    msg.cmd,
                        id:     msg.id,
                        result: 'not authenticated',
                        salt:   authList[id].salt
                    }, rinfo);
                }
                break;

            default:
                send({
                    cmd:     msg.cmd,
                    id:      msg.id,
                    result:  'unknown command'
                }, rinfo);
                break;
        }
    }

    this.init = function () {
        stopped = false;
        if (initTimer) {
            clearTimeout(initTimer);
            initTimer = null;
        }

        if (count > 10) {
            return logger.warn('host.' + hostname + ' Multi-host discovery server: Port ' + PORT + ' is occupied. Service stopped.');
        }

        server = dgram.createSocket({type: 'udp4', reuseAddr: true});

        server.on('error', err => {
            logger.error('host.' + hostname + ' Multi-host discovery server: error: ' + err.stack);
            server.close();
            server = null;

            initTimer = initTimer || setTimeout(() => {
                initTimer = null;
                this.init();
            }, 5000);
        });

        server.on('close', _err => {
            server = null;

            if (!initTimer && !stopped) {
                initTimer = setTimeout(() => {
                    initTimer = null;
                    this.init();
                }, 5000);
            }
        });

        server.on('message', (msg, rinfo) => {
            // following messages are allowed
            const text = msg.toString();
            const now  = new Date().getTime();
            const id = rinfo.address + ':' + rinfo.port;

            for (const ids in buffer) {
                if (!lastFrame[ids]) {
                    delete buffer[ids];
                } else if (now - lastFrame[ids] > 1000) {
                    delete buffer[ids];
                    delete lastFrame[ids];
                }
            }

            if (lastFrame[id] && now - lastFrame[id] > 1000) {
                buffer[id] = '';
            }

            lastFrame[id] = now;

            if (!buffer[id] && text[0] !== '{') {
                // ignore message
                logger.debug(`host.${hostname} Multi-host discovery server: Message from ${rinfo.address} ignored: ${text}`);
            } else {
                buffer[id] = (buffer[id] || '') + msg.toString();
                if (buffer[id] && buffer[id][buffer[id].length - 1] === '}') {
                    try {
                        const data = JSON.parse(buffer[id]);
                        buffer[id] = '';
                        if (data) {
                            process(data, rinfo);
                        }
                    } catch {
                        // may be not yet complete.
                    }
                }
            }
        });

        server.on('listening', () => {
            try {
                server.addMembership(MULTICAST_ADDR);
            } catch {
                logger.warn(`host.${hostname} Multi-host discovery server: Multicast membership could not be added.`);
            }
            const address = server.address();
            logger.info(`host.${hostname} Multi-host discovery server: service started on ${address.address}:${address.port}`);
        });

        server.bind(PORT);
    };

    this.close = function (callback) {
        stopped = true;
        if (initTimer) {
            clearTimeout(initTimer);
            initTimer = null;
        }
        if (server) {
            try {
                server.close(callback);
                server = null;
            } catch {
                server = null;
                callback && callback();
            }
        } else if (callback) {
            callback();
        }
    };

    this.init();

    return this;
}

module.exports = MHServer;
<|endoftext|>


<commit_before>var spawn = require('child_process').spawn;
var path = require('path');
var fs = require('fs');

var exists = fs.exists || fs.access;
var existsSync = fs.existsSync || fs.accessSync;

var path2fmpp = path.join(__dirname, './bin/fmpp' + (process.platform === 'win32' ? '.bat' : ''));
var tmpDir = path.join(__dirname, '/./tmp/');

module.exports = function () {
    removeCache(tmpDir);

    return function (filename, data, callback) {
        var fmppConfig = data.fmppConfig;
        var rdname = randomname();
        var tmpHtml = path.join(tmpDir, rdname + '.html');
        var tmpTdd = path.join(tmpDir, rdname + '.tdd');
        var tmpConfig = path.join(tmpDir, rdname + '.fmpp');
        var json = JSON.stringify(data.json);

        if (!existsSync(tmpDir)) {
            fs.mkdirSync(tmpDir);
        }
        fs.chmodSync(tmpDir, '0777');

        fs.writeFileSync(tmpTdd, json, 'utf8');
        fs.chmodSync(tmpTdd, '0777');

        var fmppData = `tdd(${tmpTdd})`;
        /* 暴露给模板的名称和jar中包名的对应关系 */
        Object.keys(fmppConfig.jarConfig || {}).forEach((key) => {
            fmppData += `\n{${key}:eval('new ${fmppConfig.jarConfig[key]}()')}`;
        });
        var config = [
            /*
            * sourceRoot 需要是存放项目所有 views 的目录，因为模板中有可能会互相 include，fmpp 需要有权限访问所有的 view
            */
            'sourceRoot:' + data.settings.views,
            'outputFile:' + tmpHtml,
            'sourceEncoding:UTF-8',
            'outputEncoding:UTF-8',
            `data:{${fmppData}}`
        ];
        fs.writeFileSync(tmpConfig, config.join('\n'), 'utf8');
        fs.chmodSync(tmpConfig, '0777');

        // 执行命令
        // 第一个参数在 shell 脚本中会处理掉，最终传给 fmpp 的参数从第二个开始计算
        var args = [fmppConfig.jarDir, filename, '-C', tmpConfig];
        // var args = [filename, '-D', data, '-o', tmpHtml, '-S', sourceRoot, '-E', 'UTF-8'];
        var fmpp = spawn(path2fmpp, args, {});

        var errorMsg = '';
        fmpp.stdout.on('data', function (data) {
            errorMsg += data.toString();
        });

        fmpp.on('close', function (code) {
            if (!errorMsg.startsWith('*** DONE ***') || code !== 0) {
                var isError = true;
                callback(errorMsg || 'uncatched freemarker parse Error occurs in ' + filename);
            }

            // 删除缓存配置文件
            fs.unlink(tmpConfig, function () {
            });
            // 删除缓存数据文件
            fs.unlink(tmpTdd, function () {
            });
            // 删除缓存html文件并返回
            exists(tmpHtml, function (flag) {
                if (isError) return fs.unlink(tmpHtml, function () {
                });

                fs.readFile(tmpHtml, 'utf8', function (err, content) {
                    callback(err, content);
                    fs.unlink(tmpHtml, function () {
                    });
                });
            });
        });

    }
}

function randomname() {
    return 'tmp' + (+new Date);
}

function removeCache(dir) {
    try {
        var subs = fs.readdirSync(dir);
        subs.forEach(function (file) {
            var filepath = path.join(dir, file);
            console.log('清理旧缓存：' + filepath);

            if (fs.statSync(filepath).isFile()) {
                // 文件
                fs.unlink(filepath, function () {
                });
            }
            if (fs.statSync(filepath).isDirectory()) {
                // 文件夹
                fs.rmdir(filepath, function () {
                });
            }
        });
    } catch (e) {
        // ignore
    }

}
<commit_msg>防止出现没有写权限的错误<commit_after>var spawn = require('child_process').spawn;
var path = require('path');
var fs = require('fs');
var os = require('os');

var exists = fs.exists || fs.access;
var existsSync = fs.existsSync || fs.accessSync;

var path2fmpp = path.join(__dirname, './bin/fmpp' + (process.platform === 'win32' ? '.bat' : ''));
var tmpDir = os.tmpdir();

module.exports = function () {
    removeCache(tmpDir);

    return function (filename, data, callback) {
        var fmppConfig = data.fmppConfig;
        var rdname = randomname();
        var tmpHtml = path.join(tmpDir, rdname + '.html');
        var tmpTdd = path.join(tmpDir, rdname + '.tdd');
        var tmpConfig = path.join(tmpDir, rdname + '.fmpp');
        var json = JSON.stringify(data.json);

        if (!existsSync(tmpDir)) {
            fs.mkdirSync(tmpDir);
        }
        fs.chmodSync(tmpDir, '0777');

        fs.writeFileSync(tmpTdd, json, 'utf8');
        fs.chmodSync(tmpTdd, '0777');

        var fmppData = `tdd(${tmpTdd})`;
        /* 暴露给模板的名称和jar中包名的对应关系 */
        Object.keys(fmppConfig.jarConfig || {}).forEach((key) => {
            fmppData += `\n{${key}:eval('new ${fmppConfig.jarConfig[key]}()')}`;
        });
        var config = [
            /*
            * sourceRoot 需要是存放项目所有 views 的目录，因为模板中有可能会互相 include，fmpp 需要有权限访问所有的 view
            */
            'sourceRoot:' + data.settings.views,
            'outputFile:' + tmpHtml,
            'sourceEncoding:UTF-8',
            'outputEncoding:UTF-8',
            `data:{${fmppData}}`
        ];
        fs.writeFileSync(tmpConfig, config.join('\n'), 'utf8');
        fs.chmodSync(tmpConfig, '0777');

        // 执行命令
        // 第一个参数在 shell 脚本中会处理掉，最终传给 fmpp 的参数从第二个开始计算
        var args = [fmppConfig.jarDir, filename, '-C', tmpConfig];
        // var args = [filename, '-D', data, '-o', tmpHtml, '-S', sourceRoot, '-E', 'UTF-8'];
        var fmpp = spawn(path2fmpp, args, {});

        var errorMsg = '';
        fmpp.stdout.on('data', function (data) {
            errorMsg += data.toString();
        });

        fmpp.on('close', function (code) {
            if (!errorMsg.startsWith('*** DONE ***') || code !== 0) {
                var isError = true;
                callback(errorMsg || 'uncatched freemarker parse Error occurs in ' + filename);
            }

            // 删除缓存配置文件
            fs.unlink(tmpConfig, function () {
            });
            // 删除缓存数据文件
            fs.unlink(tmpTdd, function () {
            });
            // 删除缓存html文件并返回
            exists(tmpHtml, function (flag) {
                if (isError) return fs.unlink(tmpHtml, function () {
                });

                fs.readFile(tmpHtml, 'utf8', function (err, content) {
                    callback(err, content);
                    fs.unlink(tmpHtml, function () {
                    });
                });
            });
        });

    }
}

function randomname() {
    return 'tmp' + (+new Date);
}

function removeCache(dir) {
    try {
        var subs = fs.readdirSync(dir);
        subs.forEach(function (file) {
            var filepath = path.join(dir, file);
            console.log('清理旧缓存：' + filepath);

            if (fs.statSync(filepath).isFile()) {
                // 文件
                fs.unlink(filepath, function () {
                });
            }
            if (fs.statSync(filepath).isDirectory()) {
                // 文件夹
                fs.rmdir(filepath, function () {
                });
            }
        });
    } catch (e) {
        // ignore
    }

}
<|endoftext|>