<commit_before>#!/usr/bin/env node
// jshint camelcase:false

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const path = require('path');
const url = require('url');

const bodyParser = require('body-parser');
const csrf = require('csurf');
const express = require('express');
const favicon = require('serve-favicon');
const i18n = require('i18n-abide');
const morgan = require('morgan');
const openid = require('openid');
const clientSessions = require('client-sessions');
const fonts = require('connect-fonts');
const opensans = require('connect-fonts-opensans');

const caching = require('../lib/caching');
const config = require('../lib/config');
const csp = require('../lib/csp');
const email = require('../lib/email');
const logger = require('../lib/logging').logger;
const cert = require('../lib/cert');
const keys = require('../lib/keys');
const statsd = require('../lib/statsd');
const validate = require('../lib/validate');
const oidTool = require('../lib/openid-tool');
const xframe = require('../lib/xframe');

const USE_TLS = url.parse(config.get('server.publicUrl')).protocol === 'https:';

if (config.get('session.secret') === config.default('session.secret')) {
  logger.warn('*** Using ephemeral secret for signing cookies. ***');
}

// start loading, or make ephmeral keys if none exist
keys(function() {
  logger.debug('*** Keys loaded. ***');
});

var openidRP = new openid.RelyingParty(
  config.get('server.publicUrl') + '/authenticate/verify', // Verification URL
  config.get('server.openidRealm'), // Realm
  true, // Use stateless verification
  false, // Strict mode
  [ // List of extensions to enable and include
    new openid.AttributeExchange(
      {'http://axschema.org/contact/email': 'required'}),
    new openid.UserInterface({mode: 'popup'})
  ]);
const googleEndpoint = 'https://www.google.com/accounts/o8/id';

const app = express();

// -- Express Configuration --

app.set('views', path.join(__dirname, '..', 'views'));
app.set('view engine', 'ejs');

app.locals.personaUrl = config.get('server.personaUrl');
app.locals.errorInfo = undefined;

// -- Express Middleware --

app.use(statsd.middleware());
app.use(bodyParser.json());

if (USE_TLS) {
  app.use(function(req, res, next) {
    req.connection.proxySecure = true;
    res.setHeader('Strict-Transport-Security',
        'max-age=10886400; includeSubdomains');
    next();
  });
}

app.use(xframe({
  exclude: ['/provision']
}));

app.use(csp([
  '/provision',
  '/authenticate',
  '/authenticate/forward',
  '/authenticate/verify'
]));

app.use(favicon(path.join(__dirname, '..', 'static', 'i', 'favicon.ico')));

app.use(clientSessions({
  cookieName: 'session',
  secret: config.get('session.secret'),
  duration: config.get('session.duration'),
  cookie: {
    maxAge: config.get('session.duration'),
    secure: USE_TLS
  }
}));

app.use(csrf());

morgan.token('path', function(req) {
  return req.path;
});
morgan.token('safe-referrer', function(req) {
  var referrer = req.headers.referer || req.headers.referrer || '';
  var queryIdx = referrer.indexOf('?');
  return (queryIdx < 0) ? referrer : referrer.slice(0, queryIdx);
});
app.use(morgan(
  ':remote-addr - - ":method :path HTTP/:http-version" :status ' +
  ':response-time :res[content-length] ":safe-referrer" ":user-agent"',
  {
    stream: {
      write: function(x) {
        logger.info(String(x).trim());
      }
    }
  }
));

// No user-specific information. Localized or caching otherwise discouraged.
app.use(caching.revalidate([
  '/.well-known/browserid',
  '/authenticate',
  '/authenticate/forward'
]));

// User-specific information or caching prohibited.
app.use(caching.prevent([
  '/__heartbeat__',
  '/provision',
  '/provision/certify',
  '/authenticate/verify',
  '/session'
]));

app.use(i18n.abide({
  supported_languages: config.get('locale.list'),
  default_lang: config.get('locale.base'),
  debug_lang: config.get('locale.debug'),
  translation_directory: config.get('locale.dir')
}));

app.use(fonts.setup({
  fonts: [ opensans ],
  allow_origin: config.get('server.publicUrl'),
  maxage: 1000 * 24 * 60 * 60 * 180 // 180 days
}));

// -- Express Routes --

app.get('/', function (req, res) {
  res.redirect(config.get('server.personaUrl'));
});

app.get('/ver.txt', function (req, res) {
  res.redirect('/static/ver.txt');
});

app.get('/__heartbeat__', function (req, res) {
  openid.discover(googleEndpoint, true, function(err/*, providers*/) {
    if (err) {
      return res.status(500).end('bad');
    }
    res.send('ok');
  });
});

app.get('/.well-known/browserid', function (req, res) {
  keys(function(pubKey) {
    res.json({
      "public-key": JSON.parse(pubKey),
      "authentication": "/authenticate",
      "provisioning": "/provision"
    });
  });
});

app.get('/session', function(req, res) {
  // the authed email will have been normalized, but navigator.id should
  // give us the claimed email again. As long we're sure the original
  // claimed email normalizes to the authed email, we can proceed.
  var claimed = req.session.claimed;
  var proven = req.session.proven;
  res.json({
    csrf: req.csrfToken(),
    proven: email.compare(claimed, proven) && claimed
  });
});

app.get('/provision', function (req, res) {
  res.render('provision');
});

app.post('/provision/certify', validate({
    email: 'gmail',
    pubkey: 'pubkey',
    duration: 'number'
  }), function(req, res) {
    var isCorrectEmail = email.compare(req.body.email, req.session.proven);

    // trying to sign a cert? then kill this cookie while we're here.
    req.session.reset();
    if (!isCorrectEmail) {
      logger.error('Email not proven, will not sign certificate');
      statsd.increment('certification.failure.no_proof');
      return res.send(401, "Email isn't verified.");
    }
    if (!req.body.pubkey) {
      logger.error('Valid public key missing, can\'t sign it.');
      statsd.increment('certification.failure.invalid_pubkey');
      return res.send(400, "Pubkey isn't valid.");
    }
    if (req.body.duration === undefined) {
      req.body.duration = config.get('cert.minDuration');
    }

    keys(function(pubKey, privKey) {
      cert.sign({
        privkey: privKey,
        hostname: config.get('cert.domain'),
        duration: Math.min(req.body.duration, config.get('cert.maxDuration')),
        pubkey: req.body.pubkey,
        email: req.body.email // use user supplied email, not normalized email
      }, function onCert(err, cert) {
        if (err) {
          logger.error('Error signing certificate: %s', String(err));
          statsd.increment('certification.failure.signing_error');
          return res.send(500, err);
        }

        statsd.increment('certification.success');
        res.json({
          cert: cert
        });
      });
    });
  });

app.get('/authenticate', function (req, res) {
  res.render('authenticate', { title: req.gettext('Loading...') });
});

app.get('/authenticate/forward', validate({ email: 'gmail' }),
  function (req, res) {
    // if there is no email address, a valid email wasn't sent
    if (!req.query.email) {
      logger.error('Authentication forwarding attempted with bad input');
      statsd.increment('authentication.forwarding.failure.bad_input');
      return res.status(400).render('error', {
        title: req.gettext('Error'),
        errorInfo: 'Invalid or missing email.'
      });
    }

    openidRP.authenticate(googleEndpoint, false, function (error, authUrl) {
      if (error || !authUrl) {
        logger.error('Auth forwarding failed [Error: %s, authUrl: %s]',
          String(error), authUrl);
        statsd.increment('authentication.forwarding.failure.openid_error');
        res.status(500).render('error', { title: req.gettext('Error') });
      } else {
        statsd.increment('authentication.forwarding.success');
        req.session.claimed = req.query.email;

        var dest = url.parse(authUrl, true);
        delete dest.search; // search takes priority over query in url.format()
        dest.query.openid_shutdown_ack = '2015-04-20';

        res.redirect(url.format(dest));
      }
    });
  });

app.get('/authenticate/verify', function (req, res) {
  // Check input precondition:
  // Session should include a valid email address that the user is claiming.
  if (!email.valid(req.session.claimed)) {
    logger.error('Invalid or missing claimed email');
    statsd.increment('authentication.openid.failure.no_claim');
    return res.status(400).render('error',
      { title: req.gettext('Error'), errorInfo: 'Invalid or missing claim.' });
  }

  // Check input precondition:
  // OpenID params must include only one email address, and it must be signed.
  if (!oidTool.validParams(req.query)) {
    return res.status(401).render('error',
      { title: req.gettext('Error'), errorInfo: 'Email not signed.' });
  }

  openidRP.verifyAssertion(req, function (error, result) {
    if (error && error.message === 'Authentication cancelled') {
      logger.info('User cancelled during openid dialog');
      statsd.increment('authentication.openid.failure.cancelled');
      res.render('authenticate_finish',
        { title: req.gettext('Loading...'), success: false });
    } else if (error || !result.authenticated || !email.valid(result.email)) {
      if (!error) {
        error = new Error('Not authenticated');
      }
      logger.error('OpenID verification failed: %s', String(error));
      statsd.increment('authentication.openid.failure.bad_result');
      res.status(403).render('error',
        { title: req.gettext('Error'), errorInfo: error.message });
    } else if (email.compare(req.session.claimed, result.email)) {
      statsd.increment('authentication.openid.success');
      req.session.proven = result.email;
      res.render('authenticate_finish',
        { title: req.gettext('Loading...'), success: true });
    } else {
      logger.info('User accounts do no match');
      statsd.increment('authentication.openid.failure.mismatch');
      res.status(409).render('error_mismatch',
        { title: req.gettext("Accounts don't match"),
          claimed: req.session.claimed, proven: result.email });
    }
  });
});

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

// -- Module Setup --

if (require.main === module) {
  var server = app.listen(config.get('server.port'), config.get('server.host'),
    function() {
      var a = server.address();
      logger.info("persona-gmail-bridge running on http://"
        + a.address + ":" + a.port);
    }
  );
}

module.exports = app;

// expose openidRP so we can mock in tests
app.setOpenIDRP = function setOpenIDRP(rp) {
  openidRP = rp;
};
<commit_msg>Set secure cookies properly behind a proxy<commit_after>#!/usr/bin/env node
// jshint camelcase:false

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const path = require('path');
const url = require('url');

const bodyParser = require('body-parser');
const csrf = require('csurf');
const express = require('express');
const favicon = require('serve-favicon');
const i18n = require('i18n-abide');
const morgan = require('morgan');
const openid = require('openid');
const clientSessions = require('client-sessions');
const fonts = require('connect-fonts');
const opensans = require('connect-fonts-opensans');

const caching = require('../lib/caching');
const config = require('../lib/config');
const csp = require('../lib/csp');
const email = require('../lib/email');
const logger = require('../lib/logging').logger;
const cert = require('../lib/cert');
const keys = require('../lib/keys');
const statsd = require('../lib/statsd');
const validate = require('../lib/validate');
const oidTool = require('../lib/openid-tool');
const xframe = require('../lib/xframe');

const USE_TLS = url.parse(config.get('server.publicUrl')).protocol === 'https:';

if (config.get('session.secret') === config.default('session.secret')) {
  logger.warn('*** Using ephemeral secret for signing cookies. ***');
}

// start loading, or make ephmeral keys if none exist
keys(function() {
  logger.debug('*** Keys loaded. ***');
});

var openidRP = new openid.RelyingParty(
  config.get('server.publicUrl') + '/authenticate/verify', // Verification URL
  config.get('server.openidRealm'), // Realm
  true, // Use stateless verification
  false, // Strict mode
  [ // List of extensions to enable and include
    new openid.AttributeExchange(
      {'http://axschema.org/contact/email': 'required'}),
    new openid.UserInterface({mode: 'popup'})
  ]);
const googleEndpoint = 'https://www.google.com/accounts/o8/id';

const app = express();

// -- Express Configuration --

app.set('views', path.join(__dirname, '..', 'views'));
app.set('view engine', 'ejs');

app.locals.personaUrl = config.get('server.personaUrl');
app.locals.errorInfo = undefined;

// -- Express Middleware --

app.use(statsd.middleware());
app.use(bodyParser.json());

if (USE_TLS) {
  app.use(function(req, res, next) {
    req.connection.proxySecure = true;
    res.setHeader('Strict-Transport-Security',
        'max-age=10886400; includeSubdomains');
    next();
  });
}

app.use(xframe({
  exclude: ['/provision']
}));

app.use(csp([
  '/provision',
  '/authenticate',
  '/authenticate/forward',
  '/authenticate/verify'
]));

app.use(favicon(path.join(__dirname, '..', 'static', 'i', 'favicon.ico')));

app.use(clientSessions({
  cookieName: 'session',
  secret: config.get('session.secret'),
  duration: config.get('session.duration'),
  cookie: {
    maxAge: config.get('session.duration'),
    secureProxy: USE_TLS
  }
}));

app.use(csrf());

morgan.token('path', function(req) {
  return req.path;
});
morgan.token('safe-referrer', function(req) {
  var referrer = req.headers.referer || req.headers.referrer || '';
  var queryIdx = referrer.indexOf('?');
  return (queryIdx < 0) ? referrer : referrer.slice(0, queryIdx);
});
app.use(morgan(
  ':remote-addr - - ":method :path HTTP/:http-version" :status ' +
  ':response-time :res[content-length] ":safe-referrer" ":user-agent"',
  {
    stream: {
      write: function(x) {
        logger.info(String(x).trim());
      }
    }
  }
));

// No user-specific information. Localized or caching otherwise discouraged.
app.use(caching.revalidate([
  '/.well-known/browserid',
  '/authenticate',
  '/authenticate/forward'
]));

// User-specific information or caching prohibited.
app.use(caching.prevent([
  '/__heartbeat__',
  '/provision',
  '/provision/certify',
  '/authenticate/verify',
  '/session'
]));

app.use(i18n.abide({
  supported_languages: config.get('locale.list'),
  default_lang: config.get('locale.base'),
  debug_lang: config.get('locale.debug'),
  translation_directory: config.get('locale.dir')
}));

app.use(fonts.setup({
  fonts: [ opensans ],
  allow_origin: config.get('server.publicUrl'),
  maxage: 1000 * 24 * 60 * 60 * 180 // 180 days
}));

// -- Express Routes --

app.get('/', function (req, res) {
  res.redirect(config.get('server.personaUrl'));
});

app.get('/ver.txt', function (req, res) {
  res.redirect('/static/ver.txt');
});

app.get('/__heartbeat__', function (req, res) {
  openid.discover(googleEndpoint, true, function(err/*, providers*/) {
    if (err) {
      return res.status(500).end('bad');
    }
    res.send('ok');
  });
});

app.get('/.well-known/browserid', function (req, res) {
  keys(function(pubKey) {
    res.json({
      "public-key": JSON.parse(pubKey),
      "authentication": "/authenticate",
      "provisioning": "/provision"
    });
  });
});

app.get('/session', function(req, res) {
  // the authed email will have been normalized, but navigator.id should
  // give us the claimed email again. As long we're sure the original
  // claimed email normalizes to the authed email, we can proceed.
  var claimed = req.session.claimed;
  var proven = req.session.proven;
  res.json({
    csrf: req.csrfToken(),
    proven: email.compare(claimed, proven) && claimed
  });
});

app.get('/provision', function (req, res) {
  res.render('provision');
});

app.post('/provision/certify', validate({
    email: 'gmail',
    pubkey: 'pubkey',
    duration: 'number'
  }), function(req, res) {
    var isCorrectEmail = email.compare(req.body.email, req.session.proven);

    // trying to sign a cert? then kill this cookie while we're here.
    req.session.reset();
    if (!isCorrectEmail) {
      logger.error('Email not proven, will not sign certificate');
      statsd.increment('certification.failure.no_proof');
      return res.send(401, "Email isn't verified.");
    }
    if (!req.body.pubkey) {
      logger.error('Valid public key missing, can\'t sign it.');
      statsd.increment('certification.failure.invalid_pubkey');
      return res.send(400, "Pubkey isn't valid.");
    }
    if (req.body.duration === undefined) {
      req.body.duration = config.get('cert.minDuration');
    }

    keys(function(pubKey, privKey) {
      cert.sign({
        privkey: privKey,
        hostname: config.get('cert.domain'),
        duration: Math.min(req.body.duration, config.get('cert.maxDuration')),
        pubkey: req.body.pubkey,
        email: req.body.email // use user supplied email, not normalized email
      }, function onCert(err, cert) {
        if (err) {
          logger.error('Error signing certificate: %s', String(err));
          statsd.increment('certification.failure.signing_error');
          return res.send(500, err);
        }

        statsd.increment('certification.success');
        res.json({
          cert: cert
        });
      });
    });
  });

app.get('/authenticate', function (req, res) {
  res.render('authenticate', { title: req.gettext('Loading...') });
});

app.get('/authenticate/forward', validate({ email: 'gmail' }),
  function (req, res) {
    // if there is no email address, a valid email wasn't sent
    if (!req.query.email) {
      logger.error('Authentication forwarding attempted with bad input');
      statsd.increment('authentication.forwarding.failure.bad_input');
      return res.status(400).render('error', {
        title: req.gettext('Error'),
        errorInfo: 'Invalid or missing email.'
      });
    }

    openidRP.authenticate(googleEndpoint, false, function (error, authUrl) {
      if (error || !authUrl) {
        logger.error('Auth forwarding failed [Error: %s, authUrl: %s]',
          String(error), authUrl);
        statsd.increment('authentication.forwarding.failure.openid_error');
        res.status(500).render('error', { title: req.gettext('Error') });
      } else {
        statsd.increment('authentication.forwarding.success');
        req.session.claimed = req.query.email;

        var dest = url.parse(authUrl, true);
        delete dest.search; // search takes priority over query in url.format()
        dest.query.openid_shutdown_ack = '2015-04-20';

        res.redirect(url.format(dest));
      }
    });
  });

app.get('/authenticate/verify', function (req, res) {
  // Check input precondition:
  // Session should include a valid email address that the user is claiming.
  if (!email.valid(req.session.claimed)) {
    logger.error('Invalid or missing claimed email');
    statsd.increment('authentication.openid.failure.no_claim');
    return res.status(400).render('error',
      { title: req.gettext('Error'), errorInfo: 'Invalid or missing claim.' });
  }

  // Check input precondition:
  // OpenID params must include only one email address, and it must be signed.
  if (!oidTool.validParams(req.query)) {
    return res.status(401).render('error',
      { title: req.gettext('Error'), errorInfo: 'Email not signed.' });
  }

  openidRP.verifyAssertion(req, function (error, result) {
    if (error && error.message === 'Authentication cancelled') {
      logger.info('User cancelled during openid dialog');
      statsd.increment('authentication.openid.failure.cancelled');
      res.render('authenticate_finish',
        { title: req.gettext('Loading...'), success: false });
    } else if (error || !result.authenticated || !email.valid(result.email)) {
      if (!error) {
        error = new Error('Not authenticated');
      }
      logger.error('OpenID verification failed: %s', String(error));
      statsd.increment('authentication.openid.failure.bad_result');
      res.status(403).render('error',
        { title: req.gettext('Error'), errorInfo: error.message });
    } else if (email.compare(req.session.claimed, result.email)) {
      statsd.increment('authentication.openid.success');
      req.session.proven = result.email;
      res.render('authenticate_finish',
        { title: req.gettext('Loading...'), success: true });
    } else {
      logger.info('User accounts do no match');
      statsd.increment('authentication.openid.failure.mismatch');
      res.status(409).render('error_mismatch',
        { title: req.gettext("Accounts don't match"),
          claimed: req.session.claimed, proven: result.email });
    }
  });
});

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

// -- Module Setup --

if (require.main === module) {
  var server = app.listen(config.get('server.port'), config.get('server.host'),
    function() {
      var a = server.address();
      logger.info("persona-gmail-bridge running on http://"
        + a.address + ":" + a.port);
    }
  );
}

module.exports = app;

// expose openidRP so we can mock in tests
app.setOpenIDRP = function setOpenIDRP(rp) {
  openidRP = rp;
};
<|endoftext|>


<commit_before>//==============================================================================
// Create and manage the menu to edit annotations.

// Hack:  When the dialog is open, the viewer still gets key events.
// The viewer/ html file will check this global before processing events.
var DIALOG_OPEN = false;

function ToggleAnnotationVisibility() {
  var vis = VIEWER1.GetAnnotationVisibility();
  if (vis == ANNOTATION_OFF) {
    vis == ANNOTATION_NO_TEXT;
  } else if (vis == ANNOTATION_NO_TEXT) {
    vis == ANNOTATION_ON;
  } else {
    vis == ANNOTATION_OFF;
  }
  SetAnnotationVisibility( vis );
}

function SetAnnotationVisibility(visibility) {
  var viewer = EVENT_MANAGER.CurrentViewer;
  if ( ! viewer) { return; }
  viewer.SetAnnotationVisibility(visibility);
  eventuallyRender();
}

function ShowAnnotationEditMenu(x, y) {
    $('#AnnotationEditMenu').css({'top': y, 'left':x}).show();
}

function InitAnnotationEditMenus() {
    // Create the menu of edit options.
    $('
').appendTo('body').css({
        'background-color': 'white',
        'border-style': 'solid',
        'border-width': '1px',
        'border-radius': '5px',
        'position': 'absolute',
        'top' : '35px',
        'left' : '35px',
        'z-index': '2',
        'color': '#303030',
        'font-size': '20px'
    }).attr('id', 'AnnotationEditMenu').hide()
      .mouseleave(function(){$(this).fadeOut();});

    var AnnotationEditSelector = $('
');
    AnnotationEditSelector.appendTo('#AnnotationEditMenu')
             .attr('id', 'AnnotationEditSelector')
             .css({'width': '100%', 'list-style-type':'none'});
    $('
').appendTo(AnnotationEditSelector)
             .text("new text")
             .click(function(){AnnotationNewText();});
    $('
').appendTo(AnnotationEditSelector)
             .text("new circle")
             .click(function(){AnnotationNewCircle();});
    $('
').appendTo(AnnotationEditSelector)
             .text("new free form")
             .click(function(){NewPolyline();});
    $('
').appendTo(AnnotationEditSelector)
             .text("save annotations")
             .click(function(){SaveAnnotations();});
    // annotation should be linked to right click anntoation button.
    // This should be in a different menu.
    $('
').appendTo(AnnotationEditSelector)
             .text("flip horizontal")
             .click(function(){FlipHorizontal();});
    $('
').appendTo(AnnotationEditSelector)
             .text("save view")
             .click(function(){SaveView();});

    // I cannot get this to work.

    //var textDiv = $('
').appendTo('body')
    //  .attr('id', 'text-properties-dialog')
    //  .attr('title', 'Text Annotation Editor');
    //var textForm = $('<form>').appendTo(textDiv);
    //$('textarea').appendTo(textDiv).attr('id', 'textwidgetcontent').css({'width':'100%', 'height':'100%'});
    //$('
').appendTo(textDiv);
    //$('Color:<input type="color" value="#0000ff">')
    //  .appendTo(textDiv)
    //  .attr('id', 'textcolor');
    //$('
').appendTo(textDiv);
    //$('<input type="checkbox" value="#0000ff">')
    //  .appendTo(textDiv)
    //  .attr('id', 'TextMarker');
    //  .value("Marker");

    //$("
 <form> <textarea id='textwidgetcontent' style='width:100%;height:100%;' ></textarea>
 Color:<input type='color' id='textcolor' //value='#0000ff'></input>
 <input type='checkbox' id='TextMarker' checked /> Marker </input>  </form>
");

    //

    //    <form>
    //      <textarea id="textwidgetcontent" style="width:100%;height:100%;" ></textarea>

    //      Color:<input type="color" id="textcolor" value="#0000ff"></input>

    //      <input type="checkbox" id="TextMarker" checked /> Marker </input>
    //    </form>
    //

    // annotation dialogs for editing properties
    $("#text-properties-dialog").dialog({
      autoOpen:false,
      height:250,
      width:350,
      modal:true,
      buttons:{
          Delete: function() {
              WidgetPropertyDialogDelete();
              DIALOG_OPEN = false;
              $(this).dialog("close");
          },
          Apply: function() {
              TextPropertyDialogApply();
              DIALOG_OPEN = false;
              $(this).dialog("close");
          }
      },
      close: function(event,ui) {
          if ( event.originalEvent && $(event.originalEvent.target).closest(".ui-dialog-titlebar-close").length ) {
              WidgetPropertyDialogCancel();
              DIALOG_OPEN = false;
              $(this).dialog("close");
          }
          $("#textwidgetcontent").val( "" ).removeClass( "ui-state-error" );
      }
    });

    $("#arrow-properties-dialog").dialog({
      autoOpen:false,
      height:280,
      width:350,
      modal:true,
      buttons:{
          Delete: function() {
              WidgetPropertyDialogDelete();
              DIALOG_OPEN = false;
              $(this).dialog("close");
          },
          Apply: function() {
              ArrowPropertyDialogApply();
              DIALOG_OPEN = false;
              $(this).dialog("close");
          }
      },
      close: function(event,ui) {
          if ( event.originalEvent && $(event.originalEvent.target).closest(".ui-dialog-titlebar-close").length ) {
              WidgetPropertyDialogCancel();
              DIALOG_OPEN = false;
              $(this).dialog("close");
          }
          //$("#arrowwidgetcontent").val( "" ).removeClass( "ui-state-error" );
      }
    });

    $("#circle-properties-dialog").dialog({
      autoOpen:false,
      height:300,
      width:350,
      modal:true,
      buttons:{
        Delete: function() {
          WidgetPropertyDialogDelete();
          DIALOG_OPEN = false;
          $(this).dialog("close");
        },
        Apply: function() {
          CirclePropertyDialogApply();
          DIALOG_OPEN = false;
          $(this).dialog("close");
        }
      },
      close: function(event,ui) {
        if ( event.originalEvent && $(event.originalEvent.target).closest(".ui-dialog-titlebar-close").length ) {
          WidgetPropertyDialogCancel();
          DIALOG_OPEN = false;
          $(this).dialog("close");
        }
        //$("#arrowwidgetcontent").val( "" ).removeClass( "ui-state-error" );
      }
    });

    $("#polyline-properties-dialog").dialog({
      autoOpen:false,
      height:250,
      width:350,
      modal:true,
      buttons:{
        Delete: function() {
          WidgetPropertyDialogDelete();
          DIALOG_OPEN = false;
          $(this).dialog("close");
        },
        Apply: function() {
          PolylinePropertyDialogApply();
          DIALOG_OPEN = false;
          $(this).dialog("close");
        }
      },
      close: function(event,ui) {
        if ( event.originalEvent && $(event.originalEvent.target).closest(".ui-dialog-titlebar-close").length ) {
          WidgetPropertyDialogCancel();
          DIALOG_OPEN = false;
          $(this).dialog("close");
        }
      }
    });

}

function SaveAnnotations() {
   $('#AnnotationEditMenu').hide();
  if ( ! EDIT) {
    return;
  }

  stack = [];
  for (i in SECTIONS) {
    inSection = SECTIONS[i];
    outSection = {"view": inSection.View, "annotations":[]};
    for (j in inSection.Annotations) {
        widget = inSection.Annotations[j];
        outSection.annotations.push(widget.Serialize());
    }
    stack.push(outSection);
  }

  $.ajax({
    type: "post",
    url: "/webgl-viewer/stack-save",
    data: {"sess": SESSID,
           "data" :  JSON.stringify( {"annotations": stack} )},
    success: function(data,status){
       //alert(data + "\nStatus: " + status);
       },
    error: function() { alert( "AJAX error(): stack-save" ); },
    });
}

function AnnotationNewText() {
  SetAnnotationVisibility(true);
  // The text is created when the apply button is pressed.
  DIALOG_OPEN = true;
  $("#text-properties-dialog").dialog("open");
  $('#AnnotationEditMenu').hide();
}

function NewPolyline() {
    SetAnnotationVisibility(true);
    // When the text button is pressed, create the widget.
    var widget = new PolylineWidget(VIEWER1, true);
    widget.Shape.SetOutlineColor(document.getElementById("polylinecolor").value);
    VIEWER1.ActiveWidget = widget;

    $('#AnnotationEditMenu').hide();
}

function AnnotationNewCircle() {
    SetAnnotationVisibility(true);
    // When the circle button is pressed, create the widget.
    var widget = new CircleWidget(VIEWER1, true);
    widget.Shape.SetOutlineColor(document.getElementById("circlecolor").value);
    VIEWER1.ActiveWidget = widget;
    $('#AnnotationEditMenu').hide();
}

function LoadWidget(viewer, serializedWidget) {
    if (serializedWidget.type == "text") {
        var widget = new TextWidget(viewer, "");
        widget.Load(serializedWidget);
        return widget;
    }
    if (serializedWidget.type == "circle") {
        var widget = new CircleWidget(viewer, false);
        widget.Load(serializedWidget);
        return widget;
    }
    if (serializedWidget.type == "polyline") {
        var widget = new PolylineWidget(viewer, false);
        widget.Load(serializedWidget);
        return widget;
    }
    return null;
}

// Stuff that really does not belong here.

// Mirror image
function FlipHorizontal() {
   $('#AnnotationEditMenu').hide();

    var cam = VIEWER1.GetCamera();
    VIEWER1.ToggleMirror();
    VIEWER1.SetCamera(cam.GetFocalPoint(), cam.GetRotation()+180.0, cam.Height);
}

// Todo: Convert this from dual view comparison to standard viewer.
function SaveView() {
  $('#AnnotationEditMenu').hide();
  var cam = VIEWER1.GetCamera();

  var messageObj = {};
  messageObj.viewid = VIEWID;
  messageObj.cam = {};

  if (cam.Mirror) {
    messageObj.cam.height = - cam.Height;
  } else {
    messageObj.cam.height = cam.Height;
  }

  // Copy values not pointer reference.
  messageObj.cam.center = [cam.FocalPoint[0], cam.FocalPoint[1]];
  messageObj.cam.rotation = 180.0 * cam.Roll / 3.14159265;

  $.ajax({
    type: "post",
    url: "/webgl-viewer/save-view",
    data: {"message" :  JSON.stringify( messageObj ),
    success: function(data,status){
       //alert(data + "\nStatus: " + status);
       },
    error: function() { alert( "AJAX - error(): save-view" ); },
    }});
}

<commit_msg>Remove all use of startup_view in view objects<commit_after><|endoftext|>


<commit_before>
$(function() {
  // set context behavior
  edit_context_listeners();
});

function edit_context_listeners() {

  $("#context").on("click", "#add-context", function() {
    add_context();
  });

  $("#context").on("click", ".context-delete", function() {
    panel = $(this).closest('.yeti-panel');
    form = panel.find('form');
    delete_context(form);
  });

  $("#context").on("click", ".more-context", function() {
    // add input row
    table = $(this).closest(".context-panel").find('table').first();
    table.append($('
<input type="text" class="inline" value="key" name="keys">	<input type="text" class="inline" name="values" value="value">
'));
  });

  $("#context").on('click', '.context-edit', function() {

    panel = $(this).closest('.yeti-panel').first();
    panel.data('replace-context-source', panel.find('.panel-heading span').text())

    // make form editable
    form = panel.find('form').first();
    form.find("th,td").each(function () {
      $(this).html($('<input type="text" class="inline">').val($(this).text().trim()));
    });
    source = panel.find(".panel-title .content")
    source.html($('<input type="text" class="inline">').val(source.text()))

    // swap controls
    panel.find(".context-edit").toggle();
    panel.find(".context-save").toggle();
    panel.find(".context-delete").toggle();
    panel.find(".more-context").toggle();

  });

  $("#context").on('click', '.context-save', function() {
    panel = $(this).closest('.yeti-panel');
    form = panel.find('form');
    // send ajax request
    save_context(panel);

    // swap controls
    panel.find(".context-edit").toggle();
    panel.find(".context-save").toggle();
    panel.find(".context-delete").toggle();
    panel.find(".more-context").toggle();

  });

}

function add_context() {

  counter = "Context" + ($("#context .context-panel").size() + 1);

  markup = $(`

<input type="text" class="inline" value="Analyst">
                  <button class="btn btn-default btn-xs pull-right more-context"> More</button>
                  <button class="btn btn-default btn-xs pull-right context-save"> Save</button>
                  <button class="btn btn-danger btn-xs pull-right context-delete" style="display: none;"> Delete</button>
                  <button class="btn btn-default btn-xs pull-right context-edit" style="display: none;"> Edit</button>

                <div class="panel-body panel-collapse" id="details-`+counter+`" role="tabpanel">
                  <form class="context-form">

<input type="text" class="inline" value="key" name="keys">	<input type="text" class="inline" name="values" value="value">

                  </form>

`);

  $("#context").append(markup);
}

function get_context_dict(form) {
  dict = {};
  form.find('table tr').each(function() {
    key = $(this).find("th input").val();
    value = $(this).find("td input").val();
    dict[key] = value;
  });

  dict['source'] = form.closest('.context-panel').find('.panel-heading input').val()

  return dict;
}

function delete_context(form) {

  dict = get_context_dict(form);

  $.ajax({
    method: "DELETE",
    headers: {"Accept": "application/json"},
    contentType: "application/json",
    url: $("#context").data('endpoint'),
    data: JSON.stringify({"context": dict}),
    success: function(data) {
      form.closest(".context-panel").remove();
    },
    error: function(data) {
      notify("Error adding context.", "danger");
    }
  });

}

function save_context(panel) {

  form = panel.find('form');
  dict = get_context_dict(form);
  old_source = panel.data('replace-context-source');

  form.find('table tr').each(function() {
    key = $(this).find("th input").val();
    value = $(this).find("td input").val();
    dict[key] = value;
  });

  if (!('source' in dict)) {
    if (form.find('[name="source"]').val() == undefined) {
      dict['source'] = 'analyst';
    }
    else {
      dict['source'] = form.find('[name="source"]').val();
    }
  }

  $.ajax({
    method: "POST",
    headers: {"Accept": "application/json"},
    contentType: "application/json",
    url: $("#context").data('endpoint'),
    data: JSON.stringify({"context": dict, "old_source": old_source}),
    success: function(data) {
      table = form.find('table tbody');
      table.empty();
      for (var i in data) {
        if (i == "source") {
          panel.find(".panel-title .content").text(data[i]);
        }
        else {
          table.append('
'+i+'	'+data[i]+'
');
        }
      }
    },
    error: function(data) {
      notify("Error adding context.", "danger");
    }
  });

}
<commit_msg>Fix #160<commit_after>
$(function() {
  // set context behavior
  edit_context_listeners();
});

function edit_context_listeners() {

  $("#context").on("click", "#add-context", function() {
    add_context();
  });

  $("#context").on("click", ".context-delete", function() {
    panel = $(this).closest('.yeti-panel');
    form = panel.find('form');
    delete_context(form);
  });

  $("#context").on("click", ".more-context", function() {
    // add input row
    table = $(this).closest(".context-panel").find('table').first();
    table.append($('
<input type="text" class="inline" value="key" name="keys">	<input type="text" class="inline" name="values" value="value">
'));
  });

  $("#context").on('click', '.context-edit', function() {

    panel = $(this).closest('.yeti-panel').first();
    panel.data('replace-context-source', panel.find('.panel-heading span').text())

    // make form editable
    form = panel.find('form').first();
    form.find("th,td").each(function () {
      $(this).html($('<input type="text" class="inline">').val($(this).text().trim()));
    });
    source = panel.find(".panel-title .content")
    source.html($('<input type="text" class="inline">').val(source.text()))

    // swap controls
    panel.find(".context-edit").toggle();
    panel.find(".context-save").toggle();
    panel.find(".context-delete").toggle();
    panel.find(".more-context").toggle();

  });

  $("#context").on('click', '.context-save', function() {
    panel = $(this).closest('.yeti-panel');
    form = panel.find('form');
    // send ajax request
    save_context(panel);

    // swap controls
    panel.find(".context-edit").toggle();
    panel.find(".context-save").toggle();
    panel.find(".context-delete").toggle();
    panel.find(".more-context").toggle();

  });

}

function add_context() {

  counter = "Context" + ($("#context .context-panel").length + 1);

  markup = $(`

<input type="text" class="inline" value="Analyst">
                  <button class="btn btn-default btn-xs pull-right more-context"> More</button>
                  <button class="btn btn-default btn-xs pull-right context-save"> Save</button>
                  <button class="btn btn-danger btn-xs pull-right context-delete" style="display: none;"> Delete</button>
                  <button class="btn btn-default btn-xs pull-right context-edit" style="display: none;"> Edit</button>

                <div class="panel-body panel-collapse" id="details-`+counter+`" role="tabpanel">
                  <form class="context-form">

<input type="text" class="inline" value="key" name="keys">	<input type="text" class="inline" name="values" value="value">

                  </form>

`);

  $("#context").append(markup);
}

function get_context_dict(form) {
  dict = {};
  form.find('table tr').each(function() {
    key = $(this).find("th input").val();
    value = $(this).find("td input").val();
    dict[key] = value;
  });

  dict['source'] = form.closest('.context-panel').find('.panel-heading input').val()

  return dict;
}

function delete_context(form) {

  dict = get_context_dict(form);

  $.ajax({
    method: "DELETE",
    headers: {"Accept": "application/json"},
    contentType: "application/json",
    url: $("#context").data('endpoint'),
    data: JSON.stringify({"context": dict}),
    success: function(data) {
      form.closest(".context-panel").remove();
    },
    error: function(data) {
      notify("Error adding context.", "danger");
    }
  });

}

function save_context(panel) {

  form = panel.find('form');
  dict = get_context_dict(form);
  old_source = panel.data('replace-context-source');

  form.find('table tr').each(function() {
    key = $(this).find("th input").val();
    value = $(this).find("td input").val();
    dict[key] = value;
  });

  if (!('source' in dict)) {
    if (form.find('[name="source"]').val() == undefined) {
      dict['source'] = 'analyst';
    }
    else {
      dict['source'] = form.find('[name="source"]').val();
    }
  }

  $.ajax({
    method: "POST",
    headers: {"Accept": "application/json"},
    contentType: "application/json",
    url: $("#context").data('endpoint'),
    data: JSON.stringify({"context": dict, "old_source": old_source}),
    success: function(data) {
      table = form.find('table tbody');
      table.empty();
      for (var i in data) {
        if (i == "source") {
          panel.find(".panel-title .content").text(data[i]);
        }
        else {
          table.append('
'+i+'	'+data[i]+'
');
        }
      }
    },
    error: function(data) {
      notify("Error adding context.", "danger");
    }
  });

}
<|endoftext|>


<commit_before>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

/*global sdata, opensocial, Config, window, alert, $ */

var sakai = sakai || {};

sakai._changepic = {};
sakai.changepic = function(tuid, placement, showSettings){

    //////////////////////
    // Config Variables //
    //////////////////////

    var realw = 0;
    var realh = 0;
    var picture = false;
    var ratio = 1;
    var userSelection = null; // The object returned by imgAreaSelect that contains the user his choice.
    var me = null;

    // These values are just in case there are no css values specified.
    // If you want to change the size of a thumbnail please do this in the CSS.
    var thumbnailWidth = 100;
    var thumbnailHeight = 100;

    //////////////
    // CSS IDS    //
    //////////////

    var containerTrigger = '#changepic_container_trigger'; // This is the id that will trigger this widget.

    // tabs
    var tabSelect = "#changepic_select";
    var tabUpload = "#changepic_upload";
    var tabSelectContent = "#changepic_selectpicture";
    var tabUploadContent = "#changepic_uploadnew";
    var tabActiveClass = "fl-tabs-active";
    var tabSearchSelected = "search_tab_selected";

    // others
    var container = "#changepic_container";
    var picForm = "#changepic_form";
    var pictureMeasurer = "#picture_measurer";
    var pictureMeasurerImage = "#picture_measurer_image";
    var saveNewSelection = "#save_new_selection";
    var fullPicture = '#changepic_fullpicture';
    var thumbnail = "#thumbnail";
    var thumbnailContainer = "#thumbnail_container";

    // An array with selectors pointing to images that need to be changed.
    var imagesToChange = ["#picture_holder img", "#myprofile_pic", "#chat_available_me .chat_available_image img"];

    ///////////////////
    // TAB FUNCTIONS //
    ///////////////////

    /**
     * Will set the upload a new picture as the viewable tab.
     * The other tab will be hidden.
     */
    var showNewTab = function(){

        $(fullPicture).imgAreaSelect({
            hide: true,
            disable: true
        });

        $(tabSelect).removeClass(tabActiveClass);
        $(tabSelect).removeClass(tabSearchSelected);

        $(tabUpload).addClass(tabSearchSelected);
        $(tabUpload).addClass(tabActiveClass);

        $(tabSelectContent).hide();
        $(tabUploadContent).show();
    };

    /**
     * Will show the tab where the user can cut out a square.
     * The upload-a-pic tab will be hidden.
     */
    var showSelectTab = function(){

        $(tabSelect).addClass(tabActiveClass);
        $(tabSelect).addClass(tabSearchSelected);

        $(tabUpload).removeClass(tabSearchSelected);
        $(tabUpload).removeClass(tabActiveClass);

        $(tabSelectContent).show();
        $(tabUploadContent).hide();
    };

    /**
     * Clicked on the upload tab
     */
    $(tabUpload).click(function(){
        showNewTab();
    });

    /**
     * Clicked on the select tab
     */
    $(tabSelect).click(function(){
        sakai._changepic.doInit();
    });

    /**
     * When the user has drawn a square this function will be called by imgAreaSelect.
     * This will draw the thumbnail by modifying it's css values.
     * @param {Object} img    The thumbnail
     * @param {Object} selection The selection object from imgAreaSelect
     */
    function preview(img, selection){

        // Save the user his selection in a global variable.
        userSelection = selection;

        // How much has the user scaled down the image?
        var scaleX = thumbnailWidth / selection.width;
        var scaleY = thumbnailHeight / selection.height;

        // Change the thumbnail according to the user his selection via CSS.
        $(thumbnail).css({
            width: Math.round(scaleX * img.width) + 'px',
            height: Math.round(scaleY * img.height) + 'px',
            marginLeft: '-' + Math.round(scaleX * selection.x1) + 'px',
            marginTop: '-' + Math.round(scaleY * selection.y1) + 'px'
        });

    }

    sakai._changepic.doInit = function(){

        // Check whether there is a base picture at all
        me = sdata.me;
        var json = me.profile;

        picture = false;

        $(picForm).attr("action", Config.URL.SDATA_FETCH_PUBLIC_URL.replace(/__USERID__/,sdata.me.user.userid));

        // Get the preferred size for the thumbnail.
        var prefThumbWidth = parseInt($(thumbnailContainer).css('width').replace(/px/gi,''), 10);
        var prefThumbHeight = parseInt($(thumbnailContainer).css('height').replace(/px/gi,''), 10);

        // Make sure we don't have 0
        thumbnailWidth  = (prefThumbWidth > 0) ? prefThumbWidth : thumbnailWidth;
        thumbnailHeight  = (prefThumbHeight > 0) ? prefThumbHeight : thumbnailHeight;

        if (json.picture) {
            picture = $.evalJSON(json.picture);
        }

        if (picture && picture._name) {
            // The user has already uploaded a picture.
            // Show the edit tab.
            // Show tab in header
            $(tabSelect).show();

            // Set the unvisible image to the full blown image. (make sure to filter the # out)
            $(pictureMeasurer).html("");

            // Check the current picture's size
            $(pictureMeasurerImage).bind("complete", function(ev){

                // save the image size in global var.
                realw = $(pictureMeasurerImage).width();
                realh = $(pictureMeasurerImage).height();

                console.log("realw: " + realw + " realh: " + realh);

                // Set the images
                $(fullPicture).attr("src", "/_user/public/" + sdata.me.user.userid + "/" + picture._name + "?sid=" + Math.random());
                $(thumbnail).attr("src", "/_user/public/" + sdata.me.user.userid + "/" + picture._name + "?sid=" + Math.random());

                // Width < 500 ; Height < 300 => set the original height and width
                if (realw < 500 && realh < 300){
                    $(fullPicture).width(realw);
                    $(fullPicture).height(realh);

                // Width > 500 ; Height < 300 => Width = 500
                } else if (realw > 500 && (realh / (realw / 500) < 300)){
                    ratio = realw / 500;
                    $(fullPicture).width(500);
                    $(fullPicture).height(Math.floor(realh / ratio));

                // Width < 500 ; Height > 300 => Height = 300
                } else if (realh > 300 && (realw / (realh / 300) < 500)) {
                    ratio = realh / 300;
                    $(fullPicture).height(300);
                    $(fullPicture).width(Math.floor(realw / ratio));

                // Width > 500 ; Height > 300
                } else if (realh > 300 && (realw / (realh / 300) > 500)) {

                    var heightonchangedwidth = realh / (realw / 500);
                    if (heightonchangedwidth > 300){
                        ratio = realh / 300;
                        $(fullPicture).height(300);
                    } else {
                        ratio = realw / 500;
                        $(fullPicture).width(500);
                    }
                }

                // If the image gets loaded, make a first selection
                var initialSelection = function(img, selection){
                    var initialSelectionHeight = realh < 100 ? realh : 100;
                    var initialSelectionWidth = realw < 100 ? realw : 100;
                    console.log(initialSelectionWidth + " " + initialSelectionHeight);
                    imageareaobject.setSelection(0,0,initialSelectionHeight, initialSelectionWidth);
                    imageareaobject.setOptions({ show: true });
                    imageareaobject.update();
                    selection = imageareaobject.getSelection();
                    preview(img, selection);
                };

                // Set the imgAreaSelect to a function so we can access it later on
                var imageareaobject = $(fullPicture).imgAreaSelect({
                    aspectRatio: "1:1",
                    disable: false,
                    hide: false,
                    instance: true,
                    onInit: initialSelection,
                    onSelectChange: preview
                });

            });
            $(pictureMeasurerImage).unbind("complete");

            showSelectTab();

        }
        else {
            // The user hasn't uploaded a picture yet.
            // Show the upload pic tab.
            $(tabSelect).hide();
            showNewTab();
        }
    };

    // This is the function that will be called when a user has cut out a selection
    // and saves it.
    $(saveNewSelection).click(function(ev){

        // The parameters for the cropit service.
        var data = {
            img: "/_user/public/" + sdata.me.user.userStoragePrefix + picture._name,
            save: "/_user/public/" + sdata.me.user.userStoragePrefix,
            x: Math.floor(userSelection.x1 * ratio),
            y: Math.floor(userSelection.y1 * ratio),
            width: Math.floor(userSelection.width * ratio),
            height:Math.floor(userSelection.height * ratio),
            dimensions: "256x256",
            "_charset_":"utf-8"
        };

        // Post all of this to the server
        $.ajax({
            url: Config.URL.IMAGE_SERVICE,
            type: "POST",
            data: data,
            success: function(data){

                var tosave = {
                    "name": "256x256_" + picture._name,
                    "_name": picture._name,
                    "_charset_":"utf-8"
                };

                var stringtosave = $.toJSON(tosave);

                sdata.me.profile.picture = stringtosave;

                // Do a patch request to the profile info so that it gets updated with the new information.
                $.ajax({
                    url: "/_user/public/" + me.user.userid + "/authprofile",
                    type : "POST",
                    data : {
                        "picture" : $.toJSON(tosave),
                        "_charset_":"utf-8"
                    },
                    success : function(data) {
                        // Change the picture in the page. (This is for my_sakai.html)
                        // Math.random is for cache issues.
                        for (var i = 0; i < imagesToChange.length;i++) {
                            $(imagesToChange[i]).attr("src", "/_user/public/" + me.user.userid + "/" + tosave.name + "?sid=" + Math.random());
                        }

                        // Hide the layover.
                        $(container).jqmHide();

                    },
                    error: function(xhr, textStatus, thrownError) {
                        alert("An error has occured");
                    }
                });

            },
            error: function(xhr, textStatus, thrownError) {
                alert("An error has occured");
            }
        });

    });

    ////////////////////////////
    // jQuery Modal functions //
    ////////////////////////////

    /**
     * Hide the layover
     * @param {Object} hash the object that represents the layover
     */
    var hideArea = function(hash){

        // Remove the selecting of an area on an image.
        $(fullPicture).imgAreaSelect({
            hide: true,
            disable: true
        });

        hash.w.hide();
        hash.o.remove();
    };

    /**
     * Show the layover.
     * @param {Object} hash
     */
    var showArea = function(hash){
        sakai._changepic.doInit();
        hash.w.show();
    };

    // This will make the widget popup as a layover.
    $(container).jqm({
        modal: true,
        trigger: containerTrigger,
        overlay: 20,
        toTop: true,
        onHide: hideArea,
        onShow: showArea
    });
};

/**
 * This method gets called the second we submit the form
 */
sakai._changepic.startCallback = function(){
    return true;
};

/**
 * When the file has been saved we will get a response back from JCR.
 * @param {Object} response
 */
sakai._changepic.completeCallback = function(response){

    // Replace any
 tags the response might contain.
    response = response.replace(/
]*>/ig,"").replace(/<\/pre[^>]*>/ig,"");

    var tosave = {
        "_name": "profilepicture"
    };

    // We edit the profile.json file with the new profile picture.
    var stringtosave = $.toJSON(tosave);

    // We edit the me object in sdata.
    // This saves a request and will be checked in the doInit function later on.
    sdata.me.profile.picture = stringtosave;

    // the object we wish to insert into the profile.json file.
    var data = {"picture":stringtosave,"_charset_":"utf-8"};

    $.ajax({
        url: Config.URL.USER_EXISTENCE_SERVICE.replace(/__USERID__.json/,sdata.me.user.userid) + ".update.html",
        type : "POST",
        data : data,
        success : function(data) {

            // we have saved the profile, now do the widgets other stuff.
            sakai._changepic.doInit();
        },
        error: function(xhr, textStatus, thrownError) {
            alert("An error has occured");
        }
    });
};

var AIM = {

    frame : function(c) {
        var n = 'f' + Math.floor(Math.random() * 99999);
        var d = document.createElement('DIV');
        d.innerHTML = '';
        document.body.appendChild(d);

        var i = document.getElementById(n);
        if (c && typeof(c.onComplete) === 'function') {
            i.onComplete = c.onComplete;
        }
        return n;
    },

    form : function(f, name) {
        f.setAttribute('target', name);
    },

    submit : function(f, c) {
        AIM.form(f, AIM.frame(c));
        if (c && typeof(c.onStart) === 'function') {
            return c.onStart();
        } else {
            return true;
        }
    },

    loaded : function(id) {
        var i = document.getElementById(id);
        var d = null;
        if (i.contentDocument) {
            d = i.contentDocument;
        } else if (i.contentWindow) {
            d = i.contentWindow.document;
        } else {
            d = window.frames[id].document;
        }
        if (d.location.href === "about:blank") {
            return;
        }

        if (typeof(i.onComplete) === 'function') {
            i.onComplete(d.body.innerHTML);
        }
    }
};

sdata.widgets.WidgetLoader.informOnLoad("changepic");<commit_msg>SAKIII-267 - fixed<commit_after>/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

/*global sdata, opensocial, Config, window, alert, $ */

var sakai = sakai || {};

sakai._changepic = {};
sakai.changepic = function(tuid, placement, showSettings){

    //////////////////////
    // Config Variables //
    //////////////////////

    var realw = 0;
    var realh = 0;
    var picture = false;
    var ratio = 1;
    var userSelection = null; // The object returned by imgAreaSelect that contains the user his choice.
    var me = null;

    // These values are just in case there are no css values specified.
    // If you want to change the size of a thumbnail please do this in the CSS.
    var thumbnailWidth = 100;
    var thumbnailHeight = 100;

    //////////////
    // CSS IDS    //
    //////////////

    var containerTrigger = '#changepic_container_trigger'; // This is the id that will trigger this widget.

    // tabs
    var tabSelect = "#changepic_select";
    var tabUpload = "#changepic_upload";
    var tabSelectContent = "#changepic_selectpicture";
    var tabUploadContent = "#changepic_uploadnew";
    var tabActiveClass = "fl-tabs-active";
    var tabSearchSelected = "search_tab_selected";

    // others
    var container = "#changepic_container";
    var picForm = "#changepic_form";
    var pictureMeasurer = "#picture_measurer";
    var pictureMeasurerImage = "#picture_measurer_image";
    var saveNewSelection = "#save_new_selection";
    var fullPicture = '#changepic_fullpicture';
    var thumbnail = "#thumbnail";
    var thumbnailContainer = "#thumbnail_container";

    // An array with selectors pointing to images that need to be changed.
    var imagesToChange = ["#picture_holder img", "#myprofile_pic", "#chat_available_me .chat_available_image img"];

    ///////////////////
    // TAB FUNCTIONS //
    ///////////////////

    /**
     * Will set the upload a new picture as the viewable tab.
     * The other tab will be hidden.
     */
    var showNewTab = function(){

        $(fullPicture).imgAreaSelect({
            hide: true,
            disable: true
        });

        $(tabSelect).removeClass(tabActiveClass);
        $(tabSelect).removeClass(tabSearchSelected);

        $(tabUpload).addClass(tabSearchSelected);
        $(tabUpload).addClass(tabActiveClass);

        $(tabSelectContent).hide();
        $(tabUploadContent).show();
    };

    /**
     * Will show the tab where the user can cut out a square.
     * The upload-a-pic tab will be hidden.
     */
    var showSelectTab = function(){

        $(tabSelect).addClass(tabActiveClass);
        $(tabSelect).addClass(tabSearchSelected);

        $(tabUpload).removeClass(tabSearchSelected);
        $(tabUpload).removeClass(tabActiveClass);

        $(tabSelectContent).show();
        $(tabUploadContent).hide();
    };

    /**
     * Clicked on the upload tab
     */
    $(tabUpload).click(function(){
        showNewTab();
    });

    /**
     * Clicked on the select tab
     */
    $(tabSelect).click(function(){
        sakai._changepic.doInit();
    });

    /**
     * When the user has drawn a square this function will be called by imgAreaSelect.
     * This will draw the thumbnail by modifying it's css values.
     * @param {Object} img    The thumbnail
     * @param {Object} selection The selection object from imgAreaSelect
     */
    function preview(img, selection){

        // Save the user his selection in a global variable.
        userSelection = selection;

        // How much has the user scaled down the image?
        var scaleX = thumbnailWidth / selection.width;
        var scaleY = thumbnailHeight / selection.height;

        // Change the thumbnail according to the user his selection via CSS.
        $(thumbnail).css({
            width: Math.round(scaleX * img.width) + 'px',
            height: Math.round(scaleY * img.height) + 'px',
            marginLeft: '-' + Math.round(scaleX * selection.x1) + 'px',
            marginTop: '-' + Math.round(scaleY * selection.y1) + 'px'
        });

    }

    sakai._changepic.doInit = function(){

        // Check whether there is a base picture at all
        me = sdata.me;
        var json = me.profile;

        picture = false;

        $(picForm).attr("action", Config.URL.SDATA_FETCH_PUBLIC_URL.replace(/__USERID__/,sdata.me.user.userid));

        // Get the preferred size for the thumbnail.
        var prefThumbWidth = parseInt($(thumbnailContainer).css('width').replace(/px/gi,''), 10);
        var prefThumbHeight = parseInt($(thumbnailContainer).css('height').replace(/px/gi,''), 10);

        // Make sure we don't have 0
        thumbnailWidth  = (prefThumbWidth > 0) ? prefThumbWidth : thumbnailWidth;
        thumbnailHeight  = (prefThumbHeight > 0) ? prefThumbHeight : thumbnailHeight;

        if (json.picture) {
            picture = $.evalJSON(json.picture);
        }

        if (picture && picture._name) {
            // The user has already uploaded a picture.
            // Show the edit tab.
            // Show tab in header
            $(tabSelect).show();

            // Set the unvisible image to the full blown image. (make sure to filter the # out)
            $(pictureMeasurer).html("");

            // Check the current picture's size
            $(pictureMeasurerImage).bind("load", function(ev){

                // save the image size in global var.
                realw = $(pictureMeasurerImage).width();
                realh = $(pictureMeasurerImage).height();

                // Set the images
                $(fullPicture).attr("src", "/_user/public/" + sdata.me.user.userid + "/" + picture._name + "?sid=" + Math.random());
                $(thumbnail).attr("src", "/_user/public/" + sdata.me.user.userid + "/" + picture._name + "?sid=" + Math.random());

                // Width < 500 ; Height < 300 => set the original height and width
                if (realw < 500 && realh < 300){
                    $(fullPicture).width(realw);
                    $(fullPicture).height(realh);

                // Width > 500 ; Height < 300 => Width = 500
                } else if (realw > 500 && (realh / (realw / 500) < 300)){
                    ratio = realw / 500;
                    $(fullPicture).width(500);
                    $(fullPicture).height(Math.floor(realh / ratio));

                // Width < 500 ; Height > 300 => Height = 300
                } else if (realh > 300 && (realw / (realh / 300) < 500)) {
                    ratio = realh / 300;
                    $(fullPicture).height(300);
                    $(fullPicture).width(Math.floor(realw / ratio));

                // Width > 500 ; Height > 300
                } else if (realh > 300 && (realw / (realh / 300) > 500)) {

                    var heightonchangedwidth = realh / (realw / 500);
                    if (heightonchangedwidth > 300){
                        ratio = realh / 300;
                        $(fullPicture).height(300);
                    } else {
                        ratio = realw / 500;
                        $(fullPicture).width(500);
                    }
                }

                // If the image gets loaded, make a first selection
                var initialSelection = function(img, selection){
                    var initialSelectionHeight = realh < 100 ? realh : 100;
                    var initialSelectionWidth = realw < 100 ? realw : 100;
                    imageareaobject.setSelection(0,0,initialSelectionHeight, initialSelectionWidth);
                    imageareaobject.setOptions({ show: true });
                    imageareaobject.update();
                    selection = imageareaobject.getSelection();
                    preview(img, selection);
                };

                // Set the imgAreaSelect to a function so we can access it later on
                var imageareaobject = $(fullPicture).imgAreaSelect({
                    aspectRatio: "1:1",
                    disable: false,
                    enable: true,
                    hide: false,
                    instance: true,
                    onInit: initialSelection,
                    onSelectChange: preview
                });
            });

            showSelectTab();

        }
        else {
            // The user hasn't uploaded a picture yet.
            // Show the upload pic tab.
            $(tabSelect).hide();
            showNewTab();
        }
    };

    // This is the function that will be called when a user has cut out a selection
    // and saves it.
    $(saveNewSelection).click(function(ev){

        // The parameters for the cropit service.
        var data = {
            img: "/_user/public/" + sdata.me.user.userStoragePrefix + picture._name,
            save: "/_user/public/" + sdata.me.user.userStoragePrefix,
            x: Math.floor(userSelection.x1 * ratio),
            y: Math.floor(userSelection.y1 * ratio),
            width: Math.floor(userSelection.width * ratio),
            height:Math.floor(userSelection.height * ratio),
            dimensions: "256x256",
            "_charset_":"utf-8"
        };

        if(data.width === 0 || data.height === 0){
            data.width = $(fullPicture).width();
            data.height = $(fullPicture).height();
            data.x = 0;
            data.y = 0;
        }

        // Post all of this to the server
        $.ajax({
            url: Config.URL.IMAGE_SERVICE,
            type: "POST",
            data: data,
            success: function(data){

                var tosave = {
                    "name": "256x256_" + picture._name,
                    "_name": picture._name,
                    "_charset_":"utf-8"
                };

                var stringtosave = $.toJSON(tosave);

                sdata.me.profile.picture = stringtosave;

                // Do a patch request to the profile info so that it gets updated with the new information.
                $.ajax({
                    url: "/_user/public/" + me.user.userid + "/authprofile",
                    type : "POST",
                    data : {
                        "picture" : $.toJSON(tosave),
                        "_charset_":"utf-8"
                    },
                    success : function(data) {
                        // Change the picture in the page. (This is for my_sakai.html)
                        // Math.random is for cache issues.
                        for (var i = 0; i < imagesToChange.length;i++) {
                            $(imagesToChange[i]).attr("src", "/_user/public/" + me.user.userid + "/" + tosave.name + "?sid=" + Math.random());
                        }

                        // Hide the layover.
                        $(container).jqmHide();

                    },
                    error: function(xhr, textStatus, thrownError) {
                        alert("An error has occured");
                    }
                });

            },
            error: function(xhr, textStatus, thrownError) {
                alert("An error has occured");
            }
        });

    });

    ////////////////////////////
    // jQuery Modal functions //
    ////////////////////////////

    /**
     * Hide the layover
     * @param {Object} hash the object that represents the layover
     */
    var hideArea = function(hash){

        // Remove the selecting of an area on an image.
        $(fullPicture).imgAreaSelect({
            hide: true,
            disable: true
        });

        hash.w.hide();
        hash.o.remove();
    };

    /**
     * Show the layover.
     * @param {Object} hash
     */
    var showArea = function(hash){
        sakai._changepic.doInit();
        hash.w.show();
    };

    // This will make the widget popup as a layover.
    $(container).jqm({
        modal: true,
        trigger: containerTrigger,
        overlay: 20,
        toTop: true,
        onHide: hideArea,
        onShow: showArea
    });
};

/**
 * This method gets called the second we submit the form
 */
sakai._changepic.startCallback = function(){
    return true;
};

/**
 * When the file has been saved we will get a response back from JCR.
 * @param {Object} response
 */
sakai._changepic.completeCallback = function(response){

    // Replace any
 tags the response might contain.
    response = response.replace(/
]*>/ig,"").replace(/<\/pre[^>]*>/ig,"");

    var tosave = {
        "_name": "profilepicture"
    };

    // We edit the profile.json file with the new profile picture.
    var stringtosave = $.toJSON(tosave);

    // We edit the me object in sdata.
    // This saves a request and will be checked in the doInit function later on.
    sdata.me.profile.picture = stringtosave;

    // the object we wish to insert into the profile.json file.
    var data = {"picture":stringtosave,"_charset_":"utf-8"};

    $.ajax({
        url: Config.URL.USER_EXISTENCE_SERVICE.replace(/__USERID__.json/,sdata.me.user.userid) + ".update.html",
        type : "POST",
        data : data,
        success : function(data) {

            // we have saved the profile, now do the widgets other stuff.
            sakai._changepic.doInit();
        },
        error: function(xhr, textStatus, thrownError) {
            alert("An error has occured");
        }
    });
};

var AIM = {

    frame : function(c) {
        var n = 'f' + Math.floor(Math.random() * 99999);
        var d = document.createElement('DIV');
        d.innerHTML = '';
        document.body.appendChild(d);

        var i = document.getElementById(n);
        if (c && typeof(c.onComplete) === 'function') {
            i.onComplete = c.onComplete;
        }
        return n;
    },

    form : function(f, name) {
        f.setAttribute('target', name);
    },

    submit : function(f, c) {
        AIM.form(f, AIM.frame(c));
        if (c && typeof(c.onStart) === 'function') {
            return c.onStart();
        } else {
            return true;
        }
    },

    loaded : function(id) {
        var i = document.getElementById(id);
        var d = null;
        if (i.contentDocument) {
            d = i.contentDocument;
        } else if (i.contentWindow) {
            d = i.contentWindow.document;
        } else {
            d = window.frames[id].document;
        }
        if (d.location.href === "about:blank") {
            return;
        }

        if (typeof(i.onComplete) === 'function') {
            i.onComplete(d.body.innerHTML);
        }
    }
};

sdata.widgets.WidgetLoader.informOnLoad("changepic");<|endoftext|>


<commit_before>var later  = require('later');
var moment = require('moment');
var sugar  = require('sugar');
var hash = require('json-hash');

require("moment-duration-format");

var bot;

var minimumCreationDelay = 0;
var minimumInterval = 60;
var noCommands = false;
var digestLength = 8;

// digest -> schedule
var schedules = {};
var lastSchedule = null;
// { 'id': <digest of data before adding timer and stuff>,
//   'created': <created timestamp>,
//   'schedule:' <later schedule>,
//   'blame': <owner>,
//   'calls': <number of calls left>,
//   'channel': <channel to print in>,
//   'command': <what to say / execute>,
//   'timer': <our timer instance if any>
//

// m1 - m2 so pass in backwards for positive
function getDifference(m1,m2) {
  return moment.duration(moment(m1).diff(moment(m2), "milliseconds"));
}

function getAverageInterval(s) {
  var numSamples = 5;
  var next = later.schedule(s).next(numSamples+2); // skip NEXT occurrence
  var totalSeconds = 0;

  next.forEach(function(e,i,d) {
    e = moment(e);
  });

  for(i=1; i<=numSamples; i++)
  {
    totalSeconds += parseInt(getDifference(next[i+1], next[i]).format("s"));
  }

  return totalSeconds/numSamples;
}

function writeSchedule(data) {
  bot.writeDataFile("later.json",
      JSON.stringify(schedules), function(err) {
        if(err) console.log("Error writing command file: " + err);
  });
}

function newSchedule(data) {
  var s;

  // reminders use cron
  if(data.calls == -1)
  {
    try {
      s = later.parse.text(data.schedule);
    } catch (ex) {
      return ex;
    }

    if(typeof(s) == "number" || s.error >= 0 )
    {
      var m = "Provided schedule query doesn't parse:\n";
      var offset = 0;

      if( typeof(s) == "number")
        offset = s;
      else
        offset = s.error;

      m += "    " + data.schedule.slice(0,offset) + '\u032D'
           + data.schedule.slice(offset,data.schedule.length);
      return m;
    }

    data.schedule = s;
  }
  else
    s = data.schedule = later.parse.cron(data.schedule);

  // check that command is valid
  //   - check if commands are disabled
  //   - doesn't call schedule
  //   - doesn't call a dumbcommand which calls schedule
  //     * checked via perceived caller
  //   - isn't too close to last call
  // check execution frequency is below some minimum (config)

  if(noCommands && data.command.substring(0, bot.config.commandPrefix.length)
      == bot.config.commandPrefix)
    return "Commands are disabled";

  if(data.command.match(/^!schedule/))
    return data.blame + ": fuck you";

  if(data.blame == bot.client.nick)
    return "Cannot call schedule recursively.";

  if(minimumCreationDelay > 0 && schedules.length > 0 && lastSchedule != 0
      && moment().diff(schedules[lastSchedule].created, 'seconds') <= minimumCreationDelay)
    return "Schedule creation is rate limited. Please wait at least " + minimumCreationDelay
           + " seconds between schedule creation.";

  if(data.calls >= 5 || data.calls == -1)
  {
    //s.range should be the range, in seconds, between schedule calls...
    //but it's undefined.
    var interval = getAverageInterval(s);
    if(minimumInterval > 0 && interval < minimumInterval)
      return "Parsed average frequency of " + moment.duration(interval, "seconds").format() + " is below the minimum "
             + "interval of " + minimumInterval + " seconds";
  }

  // Handle private message schedules. These show as channel being us--- it should
  // be them.
  if(data.channel == bot.client.nick)
    data.channel = data.blame;

  // get digest for our data
  var digest = hash.digest(data).substr(0,digestLength);
  data.id = digest;
  data.timer = null;
  schedules[digest] = data;

  lasSchedule = digest;

  registerCommand(data);
  writeSchedule(schedules);

  var next = getDifference(later.schedule(s).next(1), moment()).format();
  if(next == "0")
    next = getDifference(later.schedule(s).next(2)[1], moment()).format();

  if(next.match(/^\d+$/))
    next = next + " seconds";

  return "Created " + digest + ", the first execution is in " + next + ".";
}

function registerCommand(data) {
  var command;

  command = function() {
    var s = schedules[data.id];

    // deactivate and delete command
    if(data.calls == 0)
    {
      if(s.timer != null)
        s.timer.clear();

      delete schedules[data.id];
      writeSchedule(schedules);
      return; // command expired
    }

    if(data.target != undefined)
      bot.sayTo(data.channel, data.target + ': ' + data.command);
    else
      bot.sayTo(data.channel, data.command);
    bot.client.emit('message', bot.client.nick, data.channel,
      data.command, data.command);

    if(s.calls != -1)
    {
      s.calls -= 1;

      // write changes
      writeSchedule(schedules);
    }
  };

  schedules[data.id].timer = later.setInterval(command, data.schedule);
}

module.exports.init = function(b) {
  bot = b;
  bot.readDataFile("later.json", function(err, data) {
    if(err) {
      console.log("Initializing later.json");
      schedules = {};
      writeSchedule(schedules);
    } else {
      try {
        schedules = JSON.parse(data);

        // process schedules
        Object.keys(schedules).forEach(function(i) {
          var e = schedules[i];

          e.created = moment(e.created);
          e.timer = null;
          registerCommand(e);
        });
      } catch(ex) {
        console.log("Error parsing: " + ex);
        console.log("Corrupted later.json for schedule! Resetting file...");
        schedules = {};
        writeSchedule(schedules);
      }
    }
  });

  bot.getConfig("schedule.json", function(err, conf) {
    if(!err) {
      minimumInterval      = conf.minimum_interval;
      minimumCreationDelay = conf.minimum_creation_delay;
      noCommands           = conf.no_commands;
      digestLength         = conf.digest_length;
    }
  });
};

module.exports.commands = {
  remindme: null, // alias for remind with target from
  remind: function(r, parts, reply, command, from, to, text, raw) {
            if(parts.length < 2)
            {
              reply("Usage: !remind ([target]) \"timeframe\" \"text\"");
              reply("       Optional target defaults no one (channel wide).");
              return;
            }

            var target, schedule, command;

            if(parts.length == 2)
            {
              schedule = parts[0];
              command  = parts[1];
            }
            else
            {
              target   = parts[0];
              schedule = parts[1];
              command  = parts[2];
            }

            if(schedule.match(/in|at|after|tomorrow|next|from/i))
            {
              // use sugar to handle relative dates--- moment/later sucks at this
              var s = Date.create(schedule);
              if(!s.isValid())
                return reply("Invalid relative date provided.");
              if(!s.isFuture())
                return reply("Cannot make reminders for past dates/times.");

              // massage into a cron format
              schedule = s.utc().format('{m} {H} {d} {M} *');
            }

            reply(newSchedule({
              'blame': from,
              'created': new moment(),
              'schedule': schedule,
              'command': command,
              'channel': to,
              'calls': 1,
              'target': target
            }));
          },
  schedule: {
    _default: function(x,y,reply) {
      reply("Usage: !schedule [<add>|<remove>|<list>|<help>] [arguments]");
      reply("       Also see !schedule help <subcommand> for more details");
    },
    help: {
      _default: null, // to be aliased later to ^
      add: function(x,y,reply) {
        reply("Usage: !schedule add \"timeframe\" \"command or text\"");
        reply("       Timeframe syntax is here: http://bunkat.github.io/later/parsers.html#text");
        reply("       If a valid command isn't specified, it is treated as text to print.");
      },
      remove: function(x,y,reply) {
        reply("Usage: !schedule remove [digest]");
        reply("       Removes the schedule specified by the digest.");
      },
      list: function(x,y,reply) {
        reply("Usage: !schedule list [offset=0]");
        reply("       Provides a list of schedules, in pages, from the specified offset of 0.");
      },
    },
    add: function(r, parts, reply, command, from, to, text, raw) {
      if(parts.length !== 2) return reply("add must have *exactly* two arguments");

      reply(newSchedule({
        'blame': from,
        'created': new moment(),
        'schedule': parts[0],
        'command': parts[1],
        'channel': to,
        'calls': -1
      }));
    },
    list: function(x, parts, reply, command, from, to) {
      var offset = 0;
      var count  = 5;
      var ci     = count;
      var oi     = 0;
      var size = Object.keys(schedules).length;

      if(parts.length >= 1)
        oi = offset = parseInt(parts[0]);

      if(offset < 0)
        oi = offset = size + offset;

      if(offset >= size)
        return bot.sayTo(from, "There are only " + size + " schedules");

      if(size == 0)
        return bot.sayTo(from, "There are no schedules.");

      if(count+offset > size)
        count = size-offset;

      // count and pick
      Object.keys(schedules).forEach(function(digest) {
        if(oi > 0)
        {
          oi -= 1;
          return;
        }

        if(ci == 0)
          return;

        var e = schedules[digest];
        var channel = e.channel;

        if(!channel.match(/^(#|&)/))
          channel = "@"+channel;

        bot.sayTo(from, digest + "     " + e.blame + "     "
                + e.created.format("ddd MM/DD/YY HH:mm:ss Z")
                + "     " + channel);

        var message =  "     ";
        if( e.command.match(/^!/) )
          message += "command: " + e.command;
        else
          message += "say: " + e.command;
        bot.sayTo(from, message);

        if(ci > 0)
          ci -= 1;
      });

      var message = "Displayed schedules " + (offset+1) + "-";
      message += (offset+count) + " of " + size;

      bot.sayTo(from, message);
    },

    remove: function(r, parts, reply) {
      if(parts.length !== 1) return reply("remove must have *exactly* one argument");

      var digest = parts[0];

      if(schedules.length == 0)
        return reply("There are no schedules to delete.");

      if(schedules[digest] == undefined)
        return reply("There is no schedule for " + digest);

      var s = schedules[digest];

      if(s.timer != null)
        s.timer.clear();

      reply("Removed schedule by \"" + s.blame + "\" which runs \"" + s.command + "\"" );

      delete schedules[digest];
      writeSchedule(schedules);
    },

  }
};
// alias default for help to default
module.exports.commands.schedule.help._default
  = module.exports.commands.schedule._default;

// remindme is just remind with our name in the first part
module.exports.commands.remindme
  = function(r, parts, reply, command, from, to, text, raw) {
    parts.unshift(from);
    module.exports.commands.remind(r, parts, reply, command, from, to, text, raw);
  };
<commit_msg>Now appropriately getting schedule.length.<commit_after>var later  = require('later');
var moment = require('moment');
var sugar  = require('sugar');
var hash = require('json-hash');

require("moment-duration-format");

var bot;

var minimumCreationDelay = 0;
var minimumInterval = 60;
var noCommands = false;
var digestLength = 8;

// digest -> schedule
//TODO: make schedules a proper object w/ .length support
var schedules = {};
//FIXME: vv is dumb
var lastSchedule = null;
// { 'id': <digest of data before adding timer and stuff>,
//   'created': <created timestamp>,
//   'schedule:' <later schedule>,
//   'blame': <owner>,
//   'calls': <number of calls left>,
//   'channel': <channel to print in>,
//   'command': <what to say / execute>,
//   'timer': <our timer instance if any>
//

// m1 - m2 so pass in backwards for positive
function getDifference(m1,m2) {
  return moment.duration(moment(m1).diff(moment(m2), "milliseconds"));
}

function getAverageInterval(s) {
  var numSamples = 5;
  var next = later.schedule(s).next(numSamples+2); // skip NEXT occurrence
  var totalSeconds = 0;

  next.forEach(function(e,i,d) {
    e = moment(e);
  });

  for(i=1; i<=numSamples; i++)
  {
    totalSeconds += parseInt(getDifference(next[i+1], next[i]).format("s"));
  }

  return totalSeconds/numSamples;
}

function writeSchedule(data) {
  bot.writeDataFile("later.json",
      JSON.stringify(schedules), function(err) {
        if(err) console.log("Error writing command file: " + err);
  });
}

function newSchedule(data) {
  var s;

  // reminders use cron
  if(data.calls == -1)
  {
    try {
      s = later.parse.text(data.schedule);
    } catch (ex) {
      return ex;
    }

    if(typeof(s) == "number" || s.error >= 0 )
    {
      var m = "Provided schedule query doesn't parse:\n";
      var offset = 0;

      if( typeof(s) == "number")
        offset = s;
      else
        offset = s.error;

      m += "    " + data.schedule.slice(0,offset) + '\u032D'
           + data.schedule.slice(offset,data.schedule.length);
      return m;
    }

    data.schedule = s;
  }
  else
    s = data.schedule = later.parse.cron(data.schedule);

  // check that command is valid
  //   - check if commands are disabled
  //   - doesn't call schedule
  //   - doesn't call a dumbcommand which calls schedule
  //     * checked via perceived caller
  //   - isn't too close to last call
  // check execution frequency is below some minimum (config)

  if(noCommands && data.command.substring(0, bot.config.commandPrefix.length)
      == bot.config.commandPrefix)
    return "Commands are disabled";

  if(data.command.match(/^!schedule/))
    return data.blame + ": fuck you";

  if(data.blame == bot.client.nick)
    return "Cannot call schedule recursively.";

  if(minimumCreationDelay > 0 && Object.keys(schedules).length > 0 && lastSchedule != 0
      && moment().diff(schedules[lastSchedule].created, 'seconds') <= minimumCreationDelay)
    return "Schedule creation is rate limited. Please wait at least " + minimumCreationDelay
           + " seconds between schedule creation.";

  if(data.calls >= 5 || data.calls == -1)
  {
    //s.range should be the range, in seconds, between schedule calls...
    //but it's undefined.
    var interval = getAverageInterval(s);
    if(minimumInterval > 0 && interval < minimumInterval)
      return "Parsed average frequency of " + moment.duration(interval, "seconds").format() + " is below the minimum "
             + "interval of " + minimumInterval + " seconds";
  }

  // Handle private message schedules. These show as channel being us--- it should
  // be them.
  if(data.channel == bot.client.nick)
    data.channel = data.blame;

  // get digest for our data
  var digest = hash.digest(data).substr(0,digestLength);
  data.id = digest;
  data.timer = null;
  schedules[digest] = data;

  lasSchedule = digest;

  registerCommand(data);
  writeSchedule(schedules);

  var next = getDifference(later.schedule(s).next(1), moment()).format();
  if(next == "0")
    next = getDifference(later.schedule(s).next(2)[1], moment()).format();

  if(next.match(/^\d+$/))
    next = next + " seconds";

  return "Created " + digest + ", the first execution is in " + next + ".";
}

function registerCommand(data) {
  var command;

  command = function() {
    var s = schedules[data.id];

    // deactivate and delete command
    if(data.calls == 0)
    {
      if(s.timer != null)
        s.timer.clear();

      delete schedules[data.id];
      writeSchedule(schedules);
      return; // command expired
    }

    if(data.target != undefined)
      bot.sayTo(data.channel, data.target + ': ' + data.command);
    else
      bot.sayTo(data.channel, data.command);
    bot.client.emit('message', bot.client.nick, data.channel,
      data.command, data.command);

    if(s.calls != -1)
    {
      s.calls -= 1;

      // write changes
      writeSchedule(schedules);
    }
  };

  schedules[data.id].timer = later.setInterval(command, data.schedule);
}

module.exports.init = function(b) {
  bot = b;
  bot.readDataFile("later.json", function(err, data) {
    if(err) {
      console.log("Initializing later.json");
      schedules = {};
      writeSchedule(schedules);
    } else {
      try {
        schedules = JSON.parse(data);

        // process schedules
        Object.keys(schedules).forEach(function(i) {
          var e = schedules[i];

          e.created = moment(e.created);
          e.timer = null;
          registerCommand(e);
        });
      } catch(ex) {
        console.log("Error parsing: " + ex);
        console.log("Corrupted later.json for schedule! Resetting file...");
        schedules = {};
        writeSchedule(schedules);
      }
    }
  });

  bot.getConfig("schedule.json", function(err, conf) {
    if(!err) {
      minimumInterval      = conf.minimum_interval;
      minimumCreationDelay = conf.minimum_creation_delay;
      noCommands           = conf.no_commands;
      digestLength         = conf.digest_length;
    }
  });
};

module.exports.commands = {
  remindme: null, // alias for remind with target from
  remind: function(r, parts, reply, command, from, to, text, raw) {
            if(parts.length < 2)
            {
              reply("Usage: !remind ([target]) \"timeframe\" \"text\"");
              reply("       Optional target defaults no one (channel wide).");
              return;
            }

            var target, schedule, command;

            if(parts.length == 2)
            {
              schedule = parts[0];
              command  = parts[1];
            }
            else
            {
              target   = parts[0];
              schedule = parts[1];
              command  = parts[2];
            }

            if(schedule.match(/in|at|after|tomorrow|next|from/i))
            {
              // use sugar to handle relative dates--- moment/later sucks at this
              var s = Date.create(schedule);
              if(!s.isValid())
                return reply("Invalid relative date provided.");
              if(!s.isFuture())
                return reply("Cannot make reminders for past dates/times.");

              // massage into a cron format
              schedule = s.utc().format('{m} {H} {d} {M} *');
            }

            reply(newSchedule({
              'blame': from,
              'created': new moment(),
              'schedule': schedule,
              'command': command,
              'channel': to,
              'calls': 1,
              'target': target
            }));
          },
  schedule: {
    _default: function(x,y,reply) {
      reply("Usage: !schedule [<add>|<remove>|<list>|<help>] [arguments]");
      reply("       Also see !schedule help <subcommand> for more details");
    },
    help: {
      _default: null, // to be aliased later to ^
      add: function(x,y,reply) {
        reply("Usage: !schedule add \"timeframe\" \"command or text\"");
        reply("       Timeframe syntax is here: http://bunkat.github.io/later/parsers.html#text");
        reply("       If a valid command isn't specified, it is treated as text to print.");
      },
      remove: function(x,y,reply) {
        reply("Usage: !schedule remove [digest]");
        reply("       Removes the schedule specified by the digest.");
      },
      list: function(x,y,reply) {
        reply("Usage: !schedule list [offset=0]");
        reply("       Provides a list of schedules, in pages, from the specified offset of 0.");
      },
    },
    add: function(r, parts, reply, command, from, to, text, raw) {
      if(parts.length !== 2) return reply("add must have *exactly* two arguments");

      reply(newSchedule({
        'blame': from,
        'created': new moment(),
        'schedule': parts[0],
        'command': parts[1],
        'channel': to,
        'calls': -1
      }));
    },
    list: function(x, parts, reply, command, from, to) {
      var offset = 0;
      var count  = 5;
      var ci     = count;
      var oi     = 0;
      var size = Object.keys(schedules).length;

      if(parts.length >= 1)
        oi = offset = parseInt(parts[0]);

      if(offset < 0)
        oi = offset = size + offset;

      if(offset >= size)
        return bot.sayTo(from, "There are only " + size + " schedules");

      if(size == 0)
        return bot.sayTo(from, "There are no schedules.");

      if(count+offset > size)
        count = size-offset;

      // count and pick
      Object.keys(schedules).forEach(function(digest) {
        if(oi > 0)
        {
          oi -= 1;
          return;
        }

        if(ci == 0)
          return;

        var e = schedules[digest];
        var channel = e.channel;

        if(!channel.match(/^(#|&)/))
          channel = "@"+channel;

        bot.sayTo(from, digest + "     " + e.blame + "     "
                + e.created.format("ddd MM/DD/YY HH:mm:ss Z")
                + "     " + channel);

        var message =  "     ";
        if( e.command.match(/^!/) )
          message += "command: " + e.command;
        else
          message += "say: " + e.command;
        bot.sayTo(from, message);

        if(ci > 0)
          ci -= 1;
      });

      var message = "Displayed schedules " + (offset+1) + "-";
      message += (offset+count) + " of " + size;

      bot.sayTo(from, message);
    },

    remove: function(r, parts, reply) {
      if(parts.length !== 1) return reply("remove must have *exactly* one argument");

      var digest = parts[0];

      if(Object.keys(schedules).length == 0)
        return reply("There are no schedules to delete.");

      if(schedules[digest] == undefined)
        return reply("There is no schedule for " + digest);

      var s = schedules[digest];

      if(s.timer != null)
        s.timer.clear();

      reply("Removed schedule by \"" + s.blame + "\" which runs \"" + s.command + "\"" );

      delete schedules[digest];
      writeSchedule(schedules);
    },

  }
};
// alias default for help to default
module.exports.commands.schedule.help._default
  = module.exports.commands.schedule._default;

// remindme is just remind with our name in the first part
module.exports.commands.remindme
  = function(r, parts, reply, command, from, to, text, raw) {
    parts.unshift(from);
    module.exports.commands.remind(r, parts, reply, command, from, to, text, raw);
  };
<|endoftext|>