<commit_before>/**
 * Copyright 2013-present NightWorld.
 *
 * Licensed 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.
 */

var Client = require('./client'),
  error = require('./error'),
  runner = require('./runner'),
  token = require('./token');

module.exports = Grant;

/**
 * This is the function order used by the runner
 *
 * @type {Array}
 */
var fns = [
  extractCredentials,
  checkClient,
  checkGrantTypeAllowed,
  checkGrantType,
  checkScope,
  exposeUser,
  checkMfa,
  generateAccessToken,
  generateExpiresTime,
  saveAccessToken,
  generateRefreshToken,
  generateRefreshExpiresTime,
  saveRefreshToken,
  sendResponse
];

/**
 * Grant
 *
 * @param {Object}   config Instance of OAuth object
 * @param {Object}   req
 * @param {Object}   res
 * @param {Function} next
 */
function Grant (config, options, req, res, next) {
  this.config = config;
  this.options = options || {};
  this.model = config.model;
  this.now = new Date();
  this.req = req;
  this.res = res;

  runner(fns, this, next);
}

/**
 * Basic request validation and extraction of grant_type and client creds
 *
 * @param  {Function} done
 * @this   OAuth
 */
function extractCredentials (done) {
  // Only POST via application/x-www-form-urlencoded is acceptable
  if (this.req.method !== 'POST' ||
      !this.req.is('application/x-www-form-urlencoded')) {
    return done(error('invalid_request',
      'Method must be POST with application/x-www-form-urlencoded encoding'));
  }

  // Grant type
  this.grantType = this.req.body && this.req.body.grant_type;
  if (!this.grantType || !this.grantType.match(this.config.regex.grantType)) {
    return done(error('invalid_request',
      'Invalid or missing grant_type parameter'));
  }

  // Extract credentials
  // http://tools.ietf.org/html/rfc6749#section-3.2.1
  this.client = Client.credsFromBasic(this.req) || Client.credsFromBody(this.req);
  if (!this.client.clientId ||
      !this.client.clientId.match(this.config.regex.clientId)) {
    return done(error('invalid_client',
      'Invalid or missing client_id parameter'));
  } else if (!this.client.clientSecret) {
    return done(error('invalid_client', 'Missing client_secret parameter'));
  }

  done();
}

/**
 * Check extracted client against model
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkClient (done) {
  var self = this;

  this.model.getClient(this.client.clientId, this.client.clientSecret,
      function (err, client) {
    if (err) return done(error('server_error', false, err));

    if (!client) {
      return done(error('invalid_client', 'Client credentials are invalid'));
    }

    self.req.oauth = { client: client };

    // preserve secret, but use everything else from this method
    var secret = self.client.clientSecret;
    self.client = client;
    self.client.clientSecret = secret;

    done();
  });
}

/**
 * Delegate to the relvant grant function based on grant_type
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkGrantType (done) {
  if (this.grantType.match(/^[a-zA-Z][a-zA-Z0-9+.-]+:/)
      && this.model.extendedGrant) {
    return useExtendedGrant.call(this, done);
  }

  switch (this.grantType) {
    case 'authorization_code':
      return useAuthCodeGrant.call(this, done);
    case 'password':
      return usePasswordGrant.call(this, done);
    case 'refresh_token':
      return useRefreshTokenGrant.call(this, done);
    case 'client_credentials':
      return useClientCredentialsGrant.call(this, done);
    case 'urn:custom:mfa-otp':
      return useMfaOtpGrant.call(this, done);
    default:
      done(error('invalid_request',
        'Invalid grant_type parameter or parameter missing'));
  }
}

/**
 * Grant for authorization_code grant type
 *
 * @param  {Function} done
 */
function useAuthCodeGrant (done) {
  var code = this.req.body.code;

  if (!code) {
    return done(error('invalid_request', 'No "code" parameter'));
  }

  var self = this;
  this.model.getAuthCode(code, function (err, authCode) {
    if (err) return done(error('server_error', false, err));

    if (!authCode || authCode.clientId !== self.client.clientId) {
      return done(error('invalid_grant', 'Invalid code'));
    } else if (authCode.expires < self.now) {
      return done(error('invalid_grant', 'Code has expired'));
    }

    self.user = authCode.user || { id: authCode.userId };
    self.scope = authCode.scope;

    if (!self.user.id) {
      return done(error('server_error', false,
        'No user/userId parameter returned from getauthCode'));
    }

    done();
  });
}

/**
 * Grant for password grant type
 *
 * @param  {Function} done
 */
function usePasswordGrant (done) {
  // User credentials
  var uname = this.req.body.username,
    pword = this.req.body.password;
  if (!uname || !pword) {
    return done(error('invalid_client',
      'Missing parameters. "username" and "password" are required'));
  }

  var self = this;
  return this.model.getUser(uname, pword, function (err, user) {
    if (err) return done(error('server_error', false, err));
    if (!user) {
      return done(error('invalid_grant', 'User credentials are invalid'));
    }

    self.user = user;
    self.scope = self.req.body.scope;

    done();
  }, this.req);
}

/**
 * Grant for refresh_token grant type
 *
 * @param  {Function} done
 */
function useRefreshTokenGrant (done) {
  var token = this.req.body.refresh_token;

  if (!token) {
    return done(error('invalid_request', 'No "refresh_token" parameter'));
  }

  var self = this;
  this.model.getRefreshToken(token, function (err, refreshToken) {
    if (err) return done(error('server_error', false, err));

    if (!refreshToken || refreshToken.clientId !== self.client.clientId) {
      return done(error('invalid_grant', 'Invalid refresh token'));
    } else if (refreshToken.expires !== null &&
        refreshToken.expires < self.now) {
      return done(error('invalid_grant', 'Refresh token has expired'));
    }

    if (!refreshToken.user && !refreshToken.userId) {
      return done(error('server_error', false,
        'No user/userId parameter returned from getRefreshToken'));
    }

    self.user = refreshToken.user || { id: refreshToken.userId };
    self.scope = refreshToken.scope;

    if (self.model.revokeRefreshToken) {
      return self.model.revokeRefreshToken(token, function (err) {
        if (err) return done(error('server_error', false, err));
        done();
      });
    }

    done();
  });
}

/**
 * Grant for client_credentials grant type
 *
 * @param  {Function} done
 */
function useClientCredentialsGrant (done) {
  // Client credentials
  var clientId = this.client.clientId,
    clientSecret = this.client.clientSecret;

  if (!clientId || !clientSecret) {
    return done(error('invalid_client',
      'Missing parameters. "client_id" and "client_secret" are required'));
  }

  var self = this;
  return this.model.getUserFromClient(this.client, function (err, user) {
    if (err) return done(error('server_error', false, err));

    if (!user) {
      return done(error('invalid_grant', 'Client credentials are invalid'));
    }

    self.user = user;
    self.scope = self.req.body.scope || self.client.defaultScope;

    done();
  });
}

/**
 * Grant for extended (http://*) grant type
 *
 * @param  {Function} done
 */
function useExtendedGrant (done) {
  var self = this;
  this.model.extendedGrant(this.grantType, this.req,
      function (err, supported, user) {
    if (err) {
      return done(error(err.error || 'server_error',
        err.description || err.message, err));
    }

    if (!supported) {
      return done(error('invalid_request',
        'Invalid grant_type parameter or parameter missing'));
    } else if (!user || user.id === undefined) {
      return done(error('invalid_request', 'Invalid request.'));
    }

    self.user = user;
    done();
  });
}

/**
 * Grant for urn:custom:mfa-otp (http://*) grant type
 *
 * @param  {Function} done
 */
function useMfaOtpGrant (done) {
  var self = this;

  if (!self.req.body || !self.req.body.otp || !self.req.body.mfa_token) {
    return done(error('invalid_request', 'You must provide otp and mfa token.'));
  }

  this.model.performMfaOtp(this.req,
    function (err, user) {
      if (err) {
          return done(err);
      }

      if (user.scope) {
          self.scope = user.scope;
      }

      if (user.client) {
          self.client = user.client;
      }

      if (self.req.body && user.expires_in) {
          self.req.body.expires_in = user.expires_in
      }

      if (self.req.body && user.expires_at) {
          self.req.body.expires_at = user.expires_at
      }

      self.user = user;
      done();
  });
}

/**
 * Check the grant type is allowed for this client
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkGrantTypeAllowed (done) {
  this.model.grantTypeAllowed(this.client.clientId, this.grantType,
      function (err, allowed) {
    if (err) return done(error('server_error', false, err));

    if (!allowed) {
      return done(error('invalid_client',
        'The grant type is unauthorised for this client_id'));
    }

    done();
  });
}

/**
 * Validate the scope request
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkScope (done) {
  var self = this;
  this.model.validateScope(this.scope, this.client, this.user,
      function(err, scope, invalid) {
    if (err) return done(error('server_error', false, err));
    if (invalid) return done(error('invalid_scope', invalid));

    self.scope = scope;

    done();
  });
}

/**
 * Expose user
 *
 * @param  {Function} done
 * @this   OAuth
 */
function exposeUser (done) {
  this.req.user = this.user;

  done();
}

/**
 * Generate an access token
 *
 * @param  {Function} done
 * @this   OAuth
 */
function generateAccessToken (done) {
  var self = this;
  token(this, 'accessToken', function (err, token) {
    self.accessToken = token;
    done(err);
  });
}

function generateExpiresTime(done) {
	this.accessTokenLifetime = this.config.accessTokenLifetime;

	if (!this.model.generateExpiresTime) {
		return done();
	}

	var self = this;
	this.model.generateExpiresTime(this.req, function(err, expires) {
		if (err) {
			return done(error('server_error', false, err));
		}

		if (expires !== undefined) {
			self.accessTokenLifetime = expires;
		}
		done();
	});
}

/**
 * Save access token with model
 *
 * @param  {Function} done
 * @this   OAuth
 */
function saveAccessToken (done) {
  var accessToken = this.accessToken;

  // Object indicates a reissue
  if (typeof accessToken === 'object' && accessToken.accessToken) {
    this.accessToken = accessToken.accessToken;
    return done();
  }

  var expires = null;
  if ((this.accessTokenLifetime !== null) && (this.accessTokenLifetime > 0)) {
    expires = new Date(this.now);
    expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime);
  }

  this.model.saveAccessToken(accessToken, this.client, expires,
      this.user, this.scope, this.grantType, function (err) {
    if (err) return done(error('server_error', false, err));
    done();
  });
}

/**
 * Generate a refresh token
 *
 * @param  {Function} done
 * @this   OAuth
 */
function generateRefreshToken (done) {
  if (this.config.grants.indexOf('refresh_token') === -1) return done();

  var self = this;
  token(this, 'refreshToken', function (err, token) {
    self.refreshToken = token;
    done(err);
  });
}

function generateRefreshExpiresTime(done) {
  this.refreshTokenLifetime = this.config.refreshTokenLifetime;

  if (!this.model.generateRefreshExpiresTime) {
    return done();
  }

  var self = this;
  this.model.generateRefreshExpiresTime(this.req, function(err, expires) {
    if (err) {
      return done(error('server_error', false, err));
    }

    if (expires !== undefined) {
      self.refreshTokenLifetime = expires;
    }
    done();
  });
}

/**
 * Save refresh token with model
 *
 * @param  {Function} done
 * @this   OAuth
 */
function saveRefreshToken (done) {
  var refreshToken = this.refreshToken;

  if (!refreshToken) return done();

  // Object idicates a reissue
  if (typeof refreshToken === 'object' && refreshToken.refreshToken) {
    this.refreshToken = refreshToken.refreshToken;
    return done();
  }

  // do not issue a refresh token if non-expiring access token
  if (!this.accessTokenLifetime) {
    this.refreshToken = null;
    return done();
  }

  var expires = null;
  if (this.refreshTokenLifetime) {
    expires = new Date(this.now);
    // refresh extends past access token lifetime
    expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime + this.refreshTokenLifetime);
  }

  this.model.saveRefreshToken(refreshToken, this.client.clientId, expires,
      this.user, this.scope, function (err) {
    if (err) return done(error('server_error', false, err));
    done();
  });
}

/**
 * Check if MFA is enabled for the user. If MFA is enabled save an mfa token
 * and return an error with the mfa token
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkMfa (done) {
  if (this.user && this.user.mfaEnabled) {
    this.model.saveMfaToken(this.user, this.req, this.client.clientId, function (err, result) {
      if (err) return done(error('server_error', false, err));
      return done(error('mfa_required', result.mfa_token));
    });
  } else {
    done();
  }
}

/**
 * Sends the resulting token(s) and related information to the client
 *
 * @param  {Function} done
 * @this   OAuth
 */
function sendResponse (done) {
  var response = {
    token_type: 'bearer',
    access_token: this.accessToken
  };

  if (this.accessTokenLifetime !== null) {
    response.expires_in = this.accessTokenLifetime;
  }

  if (this.refreshToken) {
    response.refresh_token = this.refreshToken;
  }

  if (this.scope) {
    response.scope = this.scope;
  }

  if (this.options.skipResponse) {
    return done();
  }

  this.res.set({'Cache-Control': 'no-store', 'Pragma': 'no-cache'});
  this.res.jsonp(response);

  if (this.config.continueAfterResponse)
    done();
}
<commit_msg>Allow setting of null for user.expires_at<commit_after>/**
 * Copyright 2013-present NightWorld.
 *
 * Licensed 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.
 */

var Client = require('./client'),
  error = require('./error'),
  runner = require('./runner'),
  token = require('./token');

module.exports = Grant;

/**
 * This is the function order used by the runner
 *
 * @type {Array}
 */
var fns = [
  extractCredentials,
  checkClient,
  checkGrantTypeAllowed,
  checkGrantType,
  checkScope,
  exposeUser,
  checkMfa,
  generateAccessToken,
  generateExpiresTime,
  saveAccessToken,
  generateRefreshToken,
  generateRefreshExpiresTime,
  saveRefreshToken,
  sendResponse
];

/**
 * Grant
 *
 * @param {Object}   config Instance of OAuth object
 * @param {Object}   req
 * @param {Object}   res
 * @param {Function} next
 */
function Grant (config, options, req, res, next) {
  this.config = config;
  this.options = options || {};
  this.model = config.model;
  this.now = new Date();
  this.req = req;
  this.res = res;

  runner(fns, this, next);
}

/**
 * Basic request validation and extraction of grant_type and client creds
 *
 * @param  {Function} done
 * @this   OAuth
 */
function extractCredentials (done) {
  // Only POST via application/x-www-form-urlencoded is acceptable
  if (this.req.method !== 'POST' ||
      !this.req.is('application/x-www-form-urlencoded')) {
    return done(error('invalid_request',
      'Method must be POST with application/x-www-form-urlencoded encoding'));
  }

  // Grant type
  this.grantType = this.req.body && this.req.body.grant_type;
  if (!this.grantType || !this.grantType.match(this.config.regex.grantType)) {
    return done(error('invalid_request',
      'Invalid or missing grant_type parameter'));
  }

  // Extract credentials
  // http://tools.ietf.org/html/rfc6749#section-3.2.1
  this.client = Client.credsFromBasic(this.req) || Client.credsFromBody(this.req);
  if (!this.client.clientId ||
      !this.client.clientId.match(this.config.regex.clientId)) {
    return done(error('invalid_client',
      'Invalid or missing client_id parameter'));
  } else if (!this.client.clientSecret) {
    return done(error('invalid_client', 'Missing client_secret parameter'));
  }

  done();
}

/**
 * Check extracted client against model
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkClient (done) {
  var self = this;

  this.model.getClient(this.client.clientId, this.client.clientSecret,
      function (err, client) {
    if (err) return done(error('server_error', false, err));

    if (!client) {
      return done(error('invalid_client', 'Client credentials are invalid'));
    }

    self.req.oauth = { client: client };

    // preserve secret, but use everything else from this method
    var secret = self.client.clientSecret;
    self.client = client;
    self.client.clientSecret = secret;

    done();
  });
}

/**
 * Delegate to the relvant grant function based on grant_type
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkGrantType (done) {
  if (this.grantType.match(/^[a-zA-Z][a-zA-Z0-9+.-]+:/)
      && this.model.extendedGrant) {
    return useExtendedGrant.call(this, done);
  }

  switch (this.grantType) {
    case 'authorization_code':
      return useAuthCodeGrant.call(this, done);
    case 'password':
      return usePasswordGrant.call(this, done);
    case 'refresh_token':
      return useRefreshTokenGrant.call(this, done);
    case 'client_credentials':
      return useClientCredentialsGrant.call(this, done);
    case 'urn:custom:mfa-otp':
      return useMfaOtpGrant.call(this, done);
    default:
      done(error('invalid_request',
        'Invalid grant_type parameter or parameter missing'));
  }
}

/**
 * Grant for authorization_code grant type
 *
 * @param  {Function} done
 */
function useAuthCodeGrant (done) {
  var code = this.req.body.code;

  if (!code) {
    return done(error('invalid_request', 'No "code" parameter'));
  }

  var self = this;
  this.model.getAuthCode(code, function (err, authCode) {
    if (err) return done(error('server_error', false, err));

    if (!authCode || authCode.clientId !== self.client.clientId) {
      return done(error('invalid_grant', 'Invalid code'));
    } else if (authCode.expires < self.now) {
      return done(error('invalid_grant', 'Code has expired'));
    }

    self.user = authCode.user || { id: authCode.userId };
    self.scope = authCode.scope;

    if (!self.user.id) {
      return done(error('server_error', false,
        'No user/userId parameter returned from getauthCode'));
    }

    done();
  });
}

/**
 * Grant for password grant type
 *
 * @param  {Function} done
 */
function usePasswordGrant (done) {
  // User credentials
  var uname = this.req.body.username,
    pword = this.req.body.password;
  if (!uname || !pword) {
    return done(error('invalid_client',
      'Missing parameters. "username" and "password" are required'));
  }

  var self = this;
  return this.model.getUser(uname, pword, function (err, user) {
    if (err) return done(error('server_error', false, err));
    if (!user) {
      return done(error('invalid_grant', 'User credentials are invalid'));
    }

    self.user = user;
    self.scope = self.req.body.scope;

    done();
  }, this.req);
}

/**
 * Grant for refresh_token grant type
 *
 * @param  {Function} done
 */
function useRefreshTokenGrant (done) {
  var token = this.req.body.refresh_token;

  if (!token) {
    return done(error('invalid_request', 'No "refresh_token" parameter'));
  }

  var self = this;
  this.model.getRefreshToken(token, function (err, refreshToken) {
    if (err) return done(error('server_error', false, err));

    if (!refreshToken || refreshToken.clientId !== self.client.clientId) {
      return done(error('invalid_grant', 'Invalid refresh token'));
    } else if (refreshToken.expires !== null &&
        refreshToken.expires < self.now) {
      return done(error('invalid_grant', 'Refresh token has expired'));
    }

    if (!refreshToken.user && !refreshToken.userId) {
      return done(error('server_error', false,
        'No user/userId parameter returned from getRefreshToken'));
    }

    self.user = refreshToken.user || { id: refreshToken.userId };
    self.scope = refreshToken.scope;

    if (self.model.revokeRefreshToken) {
      return self.model.revokeRefreshToken(token, function (err) {
        if (err) return done(error('server_error', false, err));
        done();
      });
    }

    done();
  });
}

/**
 * Grant for client_credentials grant type
 *
 * @param  {Function} done
 */
function useClientCredentialsGrant (done) {
  // Client credentials
  var clientId = this.client.clientId,
    clientSecret = this.client.clientSecret;

  if (!clientId || !clientSecret) {
    return done(error('invalid_client',
      'Missing parameters. "client_id" and "client_secret" are required'));
  }

  var self = this;
  return this.model.getUserFromClient(this.client, function (err, user) {
    if (err) return done(error('server_error', false, err));

    if (!user) {
      return done(error('invalid_grant', 'Client credentials are invalid'));
    }

    self.user = user;
    self.scope = self.req.body.scope || self.client.defaultScope;

    done();
  });
}

/**
 * Grant for extended (http://*) grant type
 *
 * @param  {Function} done
 */
function useExtendedGrant (done) {
  var self = this;
  this.model.extendedGrant(this.grantType, this.req,
      function (err, supported, user) {
    if (err) {
      return done(error(err.error || 'server_error',
        err.description || err.message, err));
    }

    if (!supported) {
      return done(error('invalid_request',
        'Invalid grant_type parameter or parameter missing'));
    } else if (!user || user.id === undefined) {
      return done(error('invalid_request', 'Invalid request.'));
    }

    self.user = user;
    done();
  });
}

/**
 * Grant for urn:custom:mfa-otp (http://*) grant type
 *
 * @param  {Function} done
 */
function useMfaOtpGrant (done) {
  var self = this;

  if (!self.req.body || !self.req.body.otp || !self.req.body.mfa_token) {
    return done(error('invalid_request', 'You must provide otp and mfa token.'));
  }

  this.model.performMfaOtp(this.req,
    function (err, user) {
      if (err) {
          return done(err);
      }

      if (user.scope) {
          self.scope = user.scope;
      }

      if (user.client) {
          self.client = user.client;
      }

      if (self.req.body && user.expires_in) {
          self.req.body.expires_in = user.expires_in
      }

      if (self.req.body && user.expires_at !== undefined) {
          self.req.body.expires_at = user.expires_at
      }

      self.user = user;
      done();
  });
}

/**
 * Check the grant type is allowed for this client
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkGrantTypeAllowed (done) {
  this.model.grantTypeAllowed(this.client.clientId, this.grantType,
      function (err, allowed) {
    if (err) return done(error('server_error', false, err));

    if (!allowed) {
      return done(error('invalid_client',
        'The grant type is unauthorised for this client_id'));
    }

    done();
  });
}

/**
 * Validate the scope request
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkScope (done) {
  var self = this;
  this.model.validateScope(this.scope, this.client, this.user,
      function(err, scope, invalid) {
    if (err) return done(error('server_error', false, err));
    if (invalid) return done(error('invalid_scope', invalid));

    self.scope = scope;

    done();
  });
}

/**
 * Expose user
 *
 * @param  {Function} done
 * @this   OAuth
 */
function exposeUser (done) {
  this.req.user = this.user;

  done();
}

/**
 * Generate an access token
 *
 * @param  {Function} done
 * @this   OAuth
 */
function generateAccessToken (done) {
  var self = this;
  token(this, 'accessToken', function (err, token) {
    self.accessToken = token;
    done(err);
  });
}

function generateExpiresTime(done) {
	this.accessTokenLifetime = this.config.accessTokenLifetime;

	if (!this.model.generateExpiresTime) {
		return done();
	}

	var self = this;
	this.model.generateExpiresTime(this.req, function(err, expires) {
		if (err) {
			return done(error('server_error', false, err));
		}

		if (expires !== undefined) {
			self.accessTokenLifetime = expires;
		}
		done();
	});
}

/**
 * Save access token with model
 *
 * @param  {Function} done
 * @this   OAuth
 */
function saveAccessToken (done) {
  var accessToken = this.accessToken;

  // Object indicates a reissue
  if (typeof accessToken === 'object' && accessToken.accessToken) {
    this.accessToken = accessToken.accessToken;
    return done();
  }

  var expires = null;
  if ((this.accessTokenLifetime !== null) && (this.accessTokenLifetime > 0)) {
    expires = new Date(this.now);
    expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime);
  }

  this.model.saveAccessToken(accessToken, this.client, expires,
      this.user, this.scope, this.grantType, function (err) {
    if (err) return done(error('server_error', false, err));
    done();
  });
}

/**
 * Generate a refresh token
 *
 * @param  {Function} done
 * @this   OAuth
 */
function generateRefreshToken (done) {
  if (this.config.grants.indexOf('refresh_token') === -1) return done();

  var self = this;
  token(this, 'refreshToken', function (err, token) {
    self.refreshToken = token;
    done(err);
  });
}

function generateRefreshExpiresTime(done) {
  this.refreshTokenLifetime = this.config.refreshTokenLifetime;

  if (!this.model.generateRefreshExpiresTime) {
    return done();
  }

  var self = this;
  this.model.generateRefreshExpiresTime(this.req, function(err, expires) {
    if (err) {
      return done(error('server_error', false, err));
    }

    if (expires !== undefined) {
      self.refreshTokenLifetime = expires;
    }
    done();
  });
}

/**
 * Save refresh token with model
 *
 * @param  {Function} done
 * @this   OAuth
 */
function saveRefreshToken (done) {
  var refreshToken = this.refreshToken;

  if (!refreshToken) return done();

  // Object idicates a reissue
  if (typeof refreshToken === 'object' && refreshToken.refreshToken) {
    this.refreshToken = refreshToken.refreshToken;
    return done();
  }

  // do not issue a refresh token if non-expiring access token
  if (!this.accessTokenLifetime) {
    this.refreshToken = null;
    return done();
  }

  var expires = null;
  if (this.refreshTokenLifetime) {
    expires = new Date(this.now);
    // refresh extends past access token lifetime
    expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime + this.refreshTokenLifetime);
  }

  this.model.saveRefreshToken(refreshToken, this.client.clientId, expires,
      this.user, this.scope, function (err) {
    if (err) return done(error('server_error', false, err));
    done();
  });
}

/**
 * Check if MFA is enabled for the user. If MFA is enabled save an mfa token
 * and return an error with the mfa token
 *
 * @param  {Function} done
 * @this   OAuth
 */
function checkMfa (done) {
  if (this.user && this.user.mfaEnabled) {
    this.model.saveMfaToken(this.user, this.req, this.client.clientId, function (err, result) {
      if (err) return done(error('server_error', false, err));
      return done(error('mfa_required', result.mfa_token));
    });
  } else {
    done();
  }
}

/**
 * Sends the resulting token(s) and related information to the client
 *
 * @param  {Function} done
 * @this   OAuth
 */
function sendResponse (done) {
  var response = {
    token_type: 'bearer',
    access_token: this.accessToken
  };

  if (this.accessTokenLifetime !== null) {
    response.expires_in = this.accessTokenLifetime;
  }

  if (this.refreshToken) {
    response.refresh_token = this.refreshToken;
  }

  if (this.scope) {
    response.scope = this.scope;
  }

  if (this.options.skipResponse) {
    return done();
  }

  this.res.set({'Cache-Control': 'no-store', 'Pragma': 'no-cache'});
  this.res.jsonp(response);

  if (this.config.continueAfterResponse)
    done();
}
<|endoftext|>


<commit_before>var spawn = require("co-child-process"),
    co = require("co"),
    watch = require("watch"),
    fs = require("fs"),
    coFsPlus = require("co-fs-plus"),
    path = require("path"),
    simhash = require("simhash")(),
    crypto = require('crypto'),
    moment = require('moment'),
    isBinaryFile = require("isbinaryfile");

var hammingDistance = function(arr1, arr2) {
    var d = 0;
    arr1.forEach(function(a, i) {
        if(a ^ arr2[i]) d++;
    });
    return d;
}

var coSimhash = function(file) {
    return function(callback) {
        if(isBinaryFile(file.path)) {
            callback(null, file);
            return;
        }
        fs.readFile(file.path, {encoding: 'UTF-8'}, function(err, content) {
            var re = /[ \n,.'":，．：＇、#/\(\)＂]/,
                tokens = content.split(re);
            tokens = tokens.filter(function(token) {
                return token.length > 0;
            });
            file.simhash = simhash(tokens);
            callback(null, file);
        });
    }
}

module.exports = function(app) {
    app.when(function *() {

        return this.config && this.storage;

    }).use(function *(next) {

        var sourceDir = this.config.source || "source/",
            argv = this.argv,
            cmd = argv[2],
            logger = this.logger;

        if(cmd === "build" || cmd === "rebuild") {

            var filePaths = yield coFsPlus.walk(sourceDir);

            // calc sha1
            var files = yield filePaths.map(function(path) {
                return function(callback) {
                    var fd = fs.createReadStream(path);
                    var hash = crypto.createHash('sha1');
                    hash.setEncoding('hex');
                    fd.on('end', function() {
                        hash.end();
                        callback(null, {
                            path: path,
                            sha1: hash.read()
                        });
                    });
                    fd.pipe(hash);
                };
            });

            // calc simhash
            files = yield files.map(coSimhash);

            // load cached files
            var cachedFiles = this.storage.getItem('files');
            cachedFiles = cachedFiles ? JSON.parse(cachedFiles) : [];
            var cachedFilesObj = {};
            cachedFiles.forEach(function(file) {
                cachedFilesObj[file.path] = file;
            });

            var removedFiles = cachedFiles.filter(function(file) {
                return filePaths.indexOf(file.path) < 0;
            });

            var newFiles = [],
                changedFiles = [],
                unchangedFiles = [];

            var now = moment().format('YYYY-MM-DD H:mm:ss Z');
            files.forEach(function(file) {
                if(!cachedFilesObj[file.path]) {
                    file.createTime = now;
                    file.modifiedTime = now;
                    newFiles.push(file);
                    return;
                }
                if(cachedFilesObj[file.path].sha1 === file.sha1) {
                    unchangedFiles.push(cachedFilesObj[file.path]);
                } else {
                    file.modifiedTime = now;
                    changedFiles.push(file);
                }
            });

            // Rename Detection
            // using Locality Sensitive Hashing (simhash)

            newFiles = newFiles.map(function(file) {
                removedFiles.forEach(function(removedFile) {
                    if(!file.simhash || !removedFile.simhash) return;
                    if(hammingDistance(file.simhash, removedFile.simhash) < 4) {
                        logger.info("Rename Detected: " + removedFile.path + " -> " + file.path);
                        logger.info("Set file.createTime to: " + removedFile.createTime);
                        file.createTime = removedFile.createTime;
                    }
                });
                return file;
            });

            if(cmd === "rebuild") {
                newFiles = changedFiles.concat(unchangedFiles).map(function(file) {
                    return {
                        path: file.path,
                        sha1: file.sha1,
                        simhash: file.simhash,
                        createTime: file.createTime,
                        modifiedTime: file.modifiedTime
                    }
                }).concat(newFiles);
                changedFiles = [];
                unchangedFiles = [];
            }

            this.newFiles = newFiles;
            this.changedFiles = changedFiles;
            this.unchangedFiles = unchangedFiles;
            this.removedFiles = removedFiles;

            yield next;

            files = this.newFiles.concat(this.changedFiles, this.unchangedFiles);
            this.storage.setItem('files', JSON.stringify(files));
        }

        if(cmd === "watch") {
            watch.watchTree(sourceDir, function (f, curr, prev) {
                if (typeof f == "object" && prev === null && curr === null) {
                    // nothing changed
                } else {
                    logger.info("Change detected: " + f + ", rebuild now.");
                    co(function *() {
                        var output = yield spawn(argv[0], process.execArgv.concat([argv[1], 'build']));
                        logger.info(output);
                    })();
                }
            })
        }
    });
}
<commit_msg>use simhash-md5 instead of simhash-crc32<commit_after>var spawn = require("co-child-process"),
    co = require("co"),
    watch = require("watch"),
    fs = require("fs"),
    coFsPlus = require("co-fs-plus"),
    path = require("path"),
    simhash = require("simhash")("md5"),
    crypto = require('crypto'),
    moment = require('moment'),
    isBinaryFile = require("isbinaryfile");

var hammingDistance = function(arr1, arr2) {
    var d = 0;
    arr1.forEach(function(a, i) {
        if(a ^ arr2[i]) d++;
    });
    return d;
}

var coSimhash = function(file) {
    return function(callback) {
        fs.readFile(file.path, {encoding: 'UTF-8'}, function(err, content) {
            var re = /[ \n,.'":，．：＇、#/\(\)＂]/,
                tokens = content.split(re);
            tokens = tokens.filter(function(token) {
                return token.length > 0;
            });
            file.simhash = simhash(tokens);
            callback(null, file);
        });
    }
}

module.exports = function(app) {
    app.when(function *() {

        return this.config && this.storage;

    }).use(function *(next) {

        var sourceDir = this.config.source || "source/",
            argv = this.argv,
            cmd = argv[2],
            logger = this.logger;

        if(cmd === "build" || cmd === "rebuild") {

            var filePaths = yield coFsPlus.walk(sourceDir);

            // calc simhash
            var files = yield filePaths.map(function(path) {
                return coSimhash({path: path});
            });

            // load cached files
            var cachedFiles = this.storage.getItem('files');
            cachedFiles = cachedFiles ? JSON.parse(cachedFiles) : [];
            var cachedFilesObj = {};
            cachedFiles.forEach(function(file) {
                cachedFilesObj[file.path] = file;
            });

            var removedFiles = cachedFiles.filter(function(file) {
                return filePaths.indexOf(file.path) < 0;
            });

            var newFiles = [],
                changedFiles = [],
                unchangedFiles = [];

            var now = moment().format('YYYY-MM-DD H:mm:ss Z');
            files.forEach(function(file) {
                if(!cachedFilesObj[file.path]) {
                    file.createTime = now;
                    file.modifiedTime = now;
                    newFiles.push(file);
                    return;
                }
                if(hammingDistance(cachedFilesObj[file.path].simhash, file.simhash) === 0) {
                    unchangedFiles.push(cachedFilesObj[file.path]);
                } else {
                    file.modifiedTime = now;
                    changedFiles.push(file);
                }
            });

            // Rename Detection
            // using Locality Sensitive Hashing (simhash)

            newFiles = newFiles.map(function(file) {

                if(isBinaryFile(file.path)) {
                    return file;
                }

                removedFiles.forEach(function(removedFile) {
                    if(!file.simhash || !removedFile.simhash) return;
                    var d = hammingDistance(file.simhash, removedFile.simhash);
                    if(d < 20) {
                        logger.info("Rename Detected: " + removedFile.path + " -> " + file.path + ", hamming distance = " + d);
                        logger.info("Set file.createTime to: " + removedFile.createTime);
                        file.createTime = removedFile.createTime;
                    }
                });
                return file;
            });

            if(cmd === "rebuild") {
                newFiles = changedFiles.concat(unchangedFiles).map(function(file) {
                    return {
                        path: file.path,
                        simhash: file.simhash,
                        createTime: file.createTime,
                        modifiedTime: file.modifiedTime
                    }
                }).concat(newFiles);
                changedFiles = [];
                unchangedFiles = [];
            }

            this.newFiles = newFiles;
            this.changedFiles = changedFiles;
            this.unchangedFiles = unchangedFiles;
            this.removedFiles = removedFiles;

            console.log(this);

            yield next;

            files = this.newFiles.concat(this.changedFiles, this.unchangedFiles);
            this.storage.setItem('files', JSON.stringify(files));
        }

        if(cmd === "watch") {
            watch.watchTree(sourceDir, function (f, curr, prev) {
                if (typeof f == "object" && prev === null && curr === null) {
                    // nothing changed
                } else {
                    logger.info("Change detected: " + f + ", rebuild now.");
                    co(function *() {
                        var output = yield spawn(argv[0], process.execArgv.concat([argv[1], 'build']));
                        logger.info(output);
                    })();
                }
            })
        }
    });
}
<|endoftext|>


<commit_before>/**
 * Copyright 2013-present NightWorld.
 *
 * Licensed 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.
 */

var auth = require('basic-auth'),
    error = require('node-restify-errors'),
    runner = require('./runner'),
    token = require('./token');

module.exports = Grant;

/**
 * This is the function order used by the runner
 *
 * @type {Array}
 */
var fns = [
  extractCredentials,
  checkClient,
  checkGrantTypeAllowed,
  checkGrantType,
  generateAccessToken,
  saveAccessToken,
  generateRefreshToken,
  saveRefreshToken,
  sendResponse
];

/**
 * Grant
 *
 * @param {Object}   config Instance of OAuth object
 * @param {Object}   req
 * @param {Object}   res
 * @param {Function} next
 */
function Grant (config, req, res, next) {
  this.config = config;
  this.model = config.model;
  this.now = new Date();
  this.req = req;
  this.res = res;

  runner(fns, this, next);
}

/**
 * Basic request validation and extraction of grant_type and client creds
 *
 * @param  {Function} next
 * @this   OAuth
 */
function extractCredentials (next) {
  // Only POST via application/x-www-form-urlencoded is acceptable
  if (this.req.method !== 'POST' ||
      !this.req.is('application/x-www-form-urlencoded')) {
    return next(new error.BadMethodError('Method must be POST with application/x-www-form-urlencoded encoding'));
  }

  // Grant type
  this.grantType = this.req.body && this.req.body.grant_type;
  if (!this.grantType || !this.grantType.match(this.config.regex.grantType)) {
    return next(new error.BadMethodError('Invalid or missing grant_type parameter'));
  }

  // Extract credentials
  // http://tools.ietf.org/html/rfc6749#section-3.2.1
  this.client = credsFromBasic(this.req) || credsFromBody(this.req);

  if (!this.client.clientId ||
      !this.client.clientId.match(this.config.regex.clientId)) {
    return next(new error.InvalidCredentialsError('Invalid or missing client_id parameter'));
  } else if (!this.client.clientSecret) {
    return next(new error.InvalidCredentialsError('Missing client_secret parameter'));
  }

  next();
}

/**
 * Client Object (internal use only)
 *
 * @param {String} id     client_id
 * @param {String} secret client_secret
 */
function Client (id, secret) {
  this.clientId = id;
  this.clientSecret = secret;
}

/**
 * Extract client creds from Basic auth
 *
 * @return {Object} Client
 */
function credsFromBasic (req) {
  var user = auth(req);

  if (!user) return false;

  return new Client(user.name, user.pass);
}

/**
 * Extract client creds from body
 *
 * @return {Object} Client
 */
function credsFromBody (req) {
  return new Client(req.body.client_id, req.body.client_secret);
}

/**
 * Check extracted client against model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function checkClient (next) {
  this.model.getClient(this.client.clientId, this.client.clientSecret,
      function (err, client) {
        if (err) return next(new error.InternalError(err));

        if (!client) {
          return next(new error.InvalidCredentialsError('Client credentials are invalid'));
        }

        next();
      });
}

/**
 * Delegate to the relvant grant function based on grant_type
 *
 * @param  {Function} next
 * @this   OAuth
 */
function checkGrantType (next) {
  if (this.grantType.match(/^http(s|):\/\//) && this.model.extendedGrant) {
    return useExtendedGrant.call(this, next);
  }

  switch (this.grantType) {
    case 'authorization_code':
      return useAuthCodeGrant.call(this, next);
    case 'password':
      return usePasswordGrant.call(this, next);
    case 'refresh_token':
      return useRefreshTokenGrant.call(this, next);
    case 'client_credentials':
      return useClientCredentialsGrant.call(this, next);
    default:
      next(new error.MissingParameterError('Invalid grant_type parameter or parameter missing'));
  }
}

/**
 * Grant for authorization_code grant type
 *
 * @param  {Function} next
 */
function useAuthCodeGrant (next) {
  var code = this.req.body.code;

  if (!code) {
    return next(new error.MissingParameterError('No "code" parameter'));
  }

  var self = this;
  this.model.getAuthCode(code, function (err, authCode) {
    if (err) return next(new error('server_error', false, err));

    if (!authCode || authCode.clientId !== self.client.clientId) {
      return next(new error.InvalidContentError('Invalid code'));
    } else if (authCode.expires < self.now) {
      return next(new error.InvalidContentError('Code has expired'));
    }

    self.user = authCode.user || { id: authCode.userId };
    if (!self.user.id) {
      return next(new error.InternalError('No user/userId parameter returned from getauthCode'));
    }

    next();
  });
}

/**
 * Grant for password grant type
 *
 * @param  {Function} next
 */
function usePasswordGrant (next) {
  // User credentials
  var uname = this.req.body.username,
      pword = this.req.body.password;
  if (!uname || !pword) {
    return next(new error.InvalidCredentialsError('Missing parameters. "username" and "password" are required'));
  }

  var self = this;
  return this.model.getUser(uname, pword, function (err, user) {
    if (err) return next(new error.InternalError(err));
    if (!user) {
      return next(new error.InvalidCredentialsError('User credentials are invalid'));
    }

    self.user = user;
    next();
  });
}

/**
 * Grant for refresh_token grant type
 *
 * @param  {Function} next
 */
function useRefreshTokenGrant (next) {
  var token = this.req.body.refresh_token;

  if (!token) {
    return next(new error.BadMethodError('No "refresh_token" parameter'));
  }

  var self = this;
  this.model.getRefreshToken(token, function (err, refreshToken) {
    if (err) return next(new error.InternalError(err));

    if (!refreshToken || refreshToken.clientId !== self.client.clientId) {
      return next(new error.BadMethodError('Invalid refresh token'));
    } else if (refreshToken.expires !== null &&
        refreshToken.expires < self.now) {
      return next(new error.BadMethodError('Refresh token has expired'));
    }

    if (!refreshToken.user && !refreshToken.userId) {
      return next(new error.InternalError('No user/userId parameter returned from getRefreshToken'));
    }

    self.user = refreshToken.user || { id: refreshToken.userId };

    if (self.model.revokeRefreshToken) {
      return self.model.revokeRefreshToken(token, function (err) {
        if (err) return next(new error.InternalError(err));
        next();
      });
    }

    next();
  });
}

/**
 * Grant for client_credentials grant type
 *
 * @param  {Function} next
 */
function useClientCredentialsGrant (next) {
  // Client credentials
  var clientId = this.client.clientId,
      clientSecret = this.client.clientSecret;

  if (!clientId || !clientSecret) {
    return next(new error.InvalidCredentialsError('Missing parameters. "client_id" and "client_secret" are required'));
  }

  var self = this;
  return this.model.getUserFromClient(clientId, clientSecret,
      function (err, user) {
        if (err) return next(new error.InternalError(err));
        if (!user) {
          return next(new error.InvalidCredentialsError('Client credentials are invalid'));
        }

        self.user = user;
        next();
      });
}

/**
 * Grant for extended (http://*) grant type
 *
 * @param  {Function} next
 */
function useExtendedGrant (next) {
  var self = this;
  this.model.extendedGrant(this.grantType, this.req,
      function (err, supported, user) {
        if (err) {
          return next(new error(err.error || 'server_error',
              err.description || err.message, err));
        }

        if (!supported) {
          return next(new error.BadMethodError('Invalid grant_type parameter or parameter missing'));
        } else if (!user || user.id === undefined) {
          return next(new error.BadMethodError('Invalid request.'));
        }

        self.user = user;
        next();
      });
}

/**
 * Check the grant type is allowed for this client
 *
 * @param  {Function} next
 * @this   OAuth
 */
function checkGrantTypeAllowed (next) {
  this.model.grantTypeAllowed(this.client.clientId, this.grantType,
      function (err, allowed) {
        if (err) return next(new error.InternalError(err));

        if (!allowed) {
          return next(new error.InvalidCredentialsError('The grant type is unauthorised for this client_id'));
        }

        next();
      });
}

/**
 * Generate an access token
 *
 * @param  {Function} next
 * @this   OAuth
 */
function generateAccessToken (next) {
  var self = this;
  token(this, 'accessToken', function (err, token) {
    self.accessToken = token;
    next(err);
  });
}

/**
 * Save access token with model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function saveAccessToken (next) {
  var accessToken = this.accessToken;

  // Object indicates a reissue
  if (typeof accessToken === 'object' && accessToken.accessToken) {
    this.accessToken = accessToken.accessToken;
    return next();
  }

  var expires = null;
  if (this.config.accessTokenLifetime !== null) {
    expires = new Date(this.now);
    expires.setSeconds(expires.getSeconds() + this.config.accessTokenLifetime);
  }

  this.model.saveAccessToken(accessToken, this.client.clientId, expires,
      this.user, function (err, token) {
        if (err) return next(new error.InternalError(err));
        next();
      });
}

/**
 * Generate a refresh token
 *
 * @param  {Function} next
 * @this   OAuth
 */
function generateRefreshToken (next) {
  if (this.config.grants.indexOf('refresh_token') === -1) return next();

  var self = this;
  token(this, 'refreshToken', function (err, token) {
    self.refreshToken = token;
    next(err);
  });
}

/**
 * Save refresh token with model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function saveRefreshToken (next) {
  var refreshToken = this.refreshToken;

  if (!refreshToken) return next();

  // Object indicates a reissue
  if (typeof refreshToken === 'object' && refreshToken.refreshToken) {
    this.refreshToken = refreshToken.refreshToken;
    return next();
  }

  var expires = null;
  if (this.config.refreshTokenLifetime !== null) {
    expires = new Date(this.now);
    expires.setSeconds(expires.getSeconds() + this.config.refreshTokenLifetime);
  }

  this.model.saveRefreshToken(refreshToken, this.client.clientId, expires,
      this.user, function (err, user) {
        if (err) return next(new error.InternalError(err));
        next();
      });
}

/**
 * Create an access token and save it with the model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function sendResponse (next) {
  var response = {
    token_type: 'bearer',
    access_token: this.accessToken
  };

  if (this.config.accessTokenLifetime !== null) {
    response.expires_in = this.config.accessTokenLifetime;
  }

  if (this.refreshToken) response.refresh_token = this.refreshToken;

  this.res
      .set('Cache-Control', 'no-store')
      .set('Pragma', 'no-cache')
      .send(response);

  if (this.config.continueAfterResponse)
    next();
}
<commit_msg>use params instead of body<commit_after>/**
 * Copyright 2013-present NightWorld.
 *
 * Licensed 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.
 */

var auth = require('basic-auth'),
    error = require('node-restify-errors'),
    runner = require('./runner'),
    token = require('./token');

module.exports = Grant;

/**
 * This is the function order used by the runner
 *
 * @type {Array}
 */
var fns = [
  extractCredentials,
  checkClient,
  checkGrantTypeAllowed,
  checkGrantType,
  generateAccessToken,
  saveAccessToken,
  generateRefreshToken,
  saveRefreshToken,
  sendResponse
];

/**
 * Grant
 *
 * @param {Object}   config Instance of OAuth object
 * @param {Object}   req
 * @param {Object}   res
 * @param {Function} next
 */
function Grant (config, req, res, next) {
  this.config = config;
  this.model = config.model;
  this.now = new Date();
  this.req = req;
  this.res = res;

  runner(fns, this, next);
}

/**
 * Basic request validation and extraction of grant_type and client creds
 *
 * @param  {Function} next
 * @this   OAuth
 */
function extractCredentials (next) {
  // Only POST via application/x-www-form-urlencoded is acceptable
  if (this.req.method !== 'POST' ||
      !this.req.is('application/x-www-form-urlencoded')) {
    return next(new error.BadMethodError('Method must be POST with application/x-www-form-urlencoded encoding'));
  }

  // Grant type
  this.grantType = this.req.params && this.req.params.grant_type;
  if (!this.grantType || !this.grantType.match(this.config.regex.grantType)) {
    return next(new error.BadMethodError('Invalid or missing grant_type parameter'));
  }

  // Extract credentials
  // http://tools.ietf.org/html/rfc6749#section-3.2.1
  this.client = credsFromBasic(this.req) || credsFromBody(this.req);

  if (!this.client.clientId ||
      !this.client.clientId.match(this.config.regex.clientId)) {
    return next(new error.InvalidCredentialsError('Invalid or missing client_id parameter'));
  } else if (!this.client.clientSecret) {
    return next(new error.InvalidCredentialsError('Missing client_secret parameter'));
  }

  next();
}

/**
 * Client Object (internal use only)
 *
 * @param {String} id     client_id
 * @param {String} secret client_secret
 */
function Client (id, secret) {
  this.clientId = id;
  this.clientSecret = secret;
}

/**
 * Extract client creds from Basic auth
 *
 * @return {Object} Client
 */
function credsFromBasic (req) {
  var user = auth(req);

  if (!user) return false;

  return new Client(user.name, user.pass);
}

/**
 * Extract client creds from body
 *
 * @return {Object} Client
 */
function credsFromBody (req) {
  return new Client(req.params.client_id, req.params.client_secret);
}

/**
 * Check extracted client against model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function checkClient (next) {
  this.model.getClient(this.client.clientId, this.client.clientSecret,
      function (err, client) {
        if (err) return next(new error.InternalError(err));

        if (!client) {
          return next(new error.InvalidCredentialsError('Client credentials are invalid'));
        }

        next();
      });
}

/**
 * Delegate to the relvant grant function based on grant_type
 *
 * @param  {Function} next
 * @this   OAuth
 */
function checkGrantType (next) {
  if (this.grantType.match(/^http(s|):\/\//) && this.model.extendedGrant) {
    return useExtendedGrant.call(this, next);
  }

  switch (this.grantType) {
    case 'authorization_code':
      return useAuthCodeGrant.call(this, next);
    case 'password':
      return usePasswordGrant.call(this, next);
    case 'refresh_token':
      return useRefreshTokenGrant.call(this, next);
    case 'client_credentials':
      return useClientCredentialsGrant.call(this, next);
    default:
      next(new error.MissingParameterError('Invalid grant_type parameter or parameter missing'));
  }
}

/**
 * Grant for authorization_code grant type
 *
 * @param  {Function} next
 */
function useAuthCodeGrant (next) {
  var code = this.req.params.code;

  if (!code) {
    return next(new error.MissingParameterError('No "code" parameter'));
  }

  var self = this;
  this.model.getAuthCode(code, function (err, authCode) {
    if (err) return next(new error('server_error', false, err));

    if (!authCode || authCode.clientId !== self.client.clientId) {
      return next(new error.InvalidContentError('Invalid code'));
    } else if (authCode.expires < self.now) {
      return next(new error.InvalidContentError('Code has expired'));
    }

    self.user = authCode.user || { id: authCode.userId };
    if (!self.user.id) {
      return next(new error.InternalError('No user/userId parameter returned from getauthCode'));
    }

    next();
  });
}

/**
 * Grant for password grant type
 *
 * @param  {Function} next
 */
function usePasswordGrant (next) {
  // User credentials
  var uname = this.req.params.username,
      pword = this.req.params.password;
  if (!uname || !pword) {
    return next(new error.InvalidCredentialsError('Missing parameters. "username" and "password" are required'));
  }

  var self = this;
  return this.model.getUser(uname, pword, function (err, user) {
    if (err) return next(new error.InternalError(err));
    if (!user) {
      return next(new error.InvalidCredentialsError('User credentials are invalid'));
    }

    self.user = user;
    next();
  });
}

/**
 * Grant for refresh_token grant type
 *
 * @param  {Function} next
 */
function useRefreshTokenGrant (next) {
  var token = this.req.params.refresh_token;

  if (!token) {
    return next(new error.BadMethodError('No "refresh_token" parameter'));
  }

  var self = this;
  this.model.getRefreshToken(token, function (err, refreshToken) {
    if (err) return next(new error.InternalError(err));

    if (!refreshToken || refreshToken.clientId !== self.client.clientId) {
      return next(new error.BadMethodError('Invalid refresh token'));
    } else if (refreshToken.expires !== null &&
        refreshToken.expires < self.now) {
      return next(new error.BadMethodError('Refresh token has expired'));
    }

    if (!refreshToken.user && !refreshToken.userId) {
      return next(new error.InternalError('No user/userId parameter returned from getRefreshToken'));
    }

    self.user = refreshToken.user || { id: refreshToken.userId };

    if (self.model.revokeRefreshToken) {
      return self.model.revokeRefreshToken(token, function (err) {
        if (err) return next(new error.InternalError(err));
        next();
      });
    }

    next();
  });
}

/**
 * Grant for client_credentials grant type
 *
 * @param  {Function} next
 */
function useClientCredentialsGrant (next) {
  // Client credentials
  var clientId = this.client.clientId,
      clientSecret = this.client.clientSecret;

  if (!clientId || !clientSecret) {
    return next(new error.InvalidCredentialsError('Missing parameters. "client_id" and "client_secret" are required'));
  }

  var self = this;
  return this.model.getUserFromClient(clientId, clientSecret,
      function (err, user) {
        if (err) return next(new error.InternalError(err));
        if (!user) {
          return next(new error.InvalidCredentialsError('Client credentials are invalid'));
        }

        self.user = user;
        next();
      });
}

/**
 * Grant for extended (http://*) grant type
 *
 * @param  {Function} next
 */
function useExtendedGrant (next) {
  var self = this;
  this.model.extendedGrant(this.grantType, this.req,
      function (err, supported, user) {
        if (err) {
          return next(new error(err.error || 'server_error',
              err.description || err.message, err));
        }

        if (!supported) {
          return next(new error.BadMethodError('Invalid grant_type parameter or parameter missing'));
        } else if (!user || user.id === undefined) {
          return next(new error.BadMethodError('Invalid request.'));
        }

        self.user = user;
        next();
      });
}

/**
 * Check the grant type is allowed for this client
 *
 * @param  {Function} next
 * @this   OAuth
 */
function checkGrantTypeAllowed (next) {
  this.model.grantTypeAllowed(this.client.clientId, this.grantType,
      function (err, allowed) {
        if (err) return next(new error.InternalError(err));

        if (!allowed) {
          return next(new error.InvalidCredentialsError('The grant type is unauthorised for this client_id'));
        }

        next();
      });
}

/**
 * Generate an access token
 *
 * @param  {Function} next
 * @this   OAuth
 */
function generateAccessToken (next) {
  var self = this;
  token(this, 'accessToken', function (err, token) {
    self.accessToken = token;
    next(err);
  });
}

/**
 * Save access token with model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function saveAccessToken (next) {
  var accessToken = this.accessToken;

  // Object indicates a reissue
  if (typeof accessToken === 'object' && accessToken.accessToken) {
    this.accessToken = accessToken.accessToken;
    return next();
  }

  var expires = null;
  if (this.config.accessTokenLifetime !== null) {
    expires = new Date(this.now);
    expires.setSeconds(expires.getSeconds() + this.config.accessTokenLifetime);
  }

  this.model.saveAccessToken(accessToken, this.client.clientId, expires,
      this.user, function (err, token) {
        if (err) return next(new error.InternalError(err));
        next();
      });
}

/**
 * Generate a refresh token
 *
 * @param  {Function} next
 * @this   OAuth
 */
function generateRefreshToken (next) {
  if (this.config.grants.indexOf('refresh_token') === -1) return next();

  var self = this;
  token(this, 'refreshToken', function (err, token) {
    self.refreshToken = token;
    next(err);
  });
}

/**
 * Save refresh token with model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function saveRefreshToken (next) {
  var refreshToken = this.refreshToken;

  if (!refreshToken) return next();

  // Object indicates a reissue
  if (typeof refreshToken === 'object' && refreshToken.refreshToken) {
    this.refreshToken = refreshToken.refreshToken;
    return next();
  }

  var expires = null;
  if (this.config.refreshTokenLifetime !== null) {
    expires = new Date(this.now);
    expires.setSeconds(expires.getSeconds() + this.config.refreshTokenLifetime);
  }

  this.model.saveRefreshToken(refreshToken, this.client.clientId, expires,
      this.user, function (err, user) {
        if (err) return next(new error.InternalError(err));
        next();
      });
}

/**
 * Create an access token and save it with the model
 *
 * @param  {Function} next
 * @this   OAuth
 */
function sendResponse (next) {
  var response = {
    token_type: 'bearer',
    access_token: this.accessToken
  };

  if (this.config.accessTokenLifetime !== null) {
    response.expires_in = this.config.accessTokenLifetime;
  }

  if (this.refreshToken) response.refresh_token = this.refreshToken;

  this.res
      .set('Cache-Control', 'no-store')
      .set('Pragma', 'no-cache')
      .send(response);

  if (this.config.continueAfterResponse)
    next();
}
<|endoftext|>


<commit_before>(function() {
	'use strict';

	angular
		.module('greyjaysEvaluation')
		.service('ModelService', ModelService);

	/** @ngInject */
	function ModelService($rootScope, $http, _) {

		var MonService = function() {
			this.all = [];
			this.url = '';
		};

		MonService.prototype.setUrl = function(params, env, strObj) {
			var monUrl = '';
			if (env === 'local') {
				monUrl = params[env].RESOURCE_URL + '/datas/' + strObj + '.json';
			} else {
				monUrl = params[env].RESOURCE_URL + '&' + params[env].PORTLET_PARAM + 'type=list&' + params[env].PORTLET_PARAM + 'entity=' + strObj;
			}
			return monUrl;
		};

		MonService.prototype.parse = function(response) {
			return response.models;
		};

		MonService.prototype.fetch = function() {
			var self = this;
			if (self.url === '') {
				throw new Error('You must specify a url on the class');
			}
			return $http.get(self.url).success(function(response) { self.all = self.parse(response); });
		};

		MonService.prototype.get = function(id) {
			return _.find(this.all, function(item) {
				return item.id === id;
			});
		};

		MonService.prototype.count = function() {
			return this.all.length;
		};

		return new MonService();

	}
})();
<commit_msg>ajout de delete/save/add dans model<commit_after>(function() {
	'use strict';

	angular
		.module('greyjaysEvaluation')
		.service('ModelService', ModelService);

	/** @ngInject */
	function ModelService($rootScope, $http, _) {

		var MonService = function() {
			this.all = [];
			this.url = '';
		};

		MonService.prototype.setUrl = function(params, env, strObj) {
			var monUrl = '';
			if (env === 'local') {
				monUrl = params[env].RESOURCE_URL + '/datas/' + strObj + '.json';
			} else {
				monUrl = params[env].RESOURCE_URL + '&' + params[env].PORTLET_PARAM + 'type=list&' + params[env].PORTLET_PARAM + 'entity=' + strObj;
			}
			return monUrl;
		};

		MonService.prototype.parse = function(response) {
			return response.models;
		};

		MonService.prototype.fetch = function() {
			var self = this;
			if (self.url === '') {
				throw new Error('You must specify a url on the class');
			}
			return $http.get(self.url).success(function(response) { self.all = self.parse(response); });
		};

		MonService.prototype.get = function(id) {
			return _.find(this.all, function(item) {
				return item.id === id;
			});
		};

		MonService.prototype.delete = function(id) {
			for (var i in this.all) {
				if (this.all[i].id == id) {
					this.all.splice(i, 1);
				}
			}
		};

		MonService.prototype.save = function (item) {
			var self = this;
			if (item.id === null) {
				//if this is new contact, add it in contacts array
				var uid = self.count();
				item.id = uid++;
				self.all.push(item);
			} else {
				//for existing contact, find this contact using id
				//and update it.
				for (var i in self.all) {
					if (self.all[i].id == item.id) {
						self.all[i] = item;
					}
				}
			}
		};

		MonService.prototype.count = function() {
			return this.all.length;
		};

		return new MonService();

	}
})();
<|endoftext|>


<commit_before>var custom = (function () {
    var api = {
        apis: [],
        name: "custom",
        name_zh: "自定义",
        type: 'custom',
        chance: 0
    }

    var exported = {
        getImageUrl: getImageUrl,
        api: api
    }
    function getImageUrl() {
        var url = _randomPick(exported.api.apis, 1).shift();
        if (url) {
            return Promise.resolve(url);
        } else {
            return Promise.reject();
        }
    }
    return exported;
})();<commit_msg>fix custom url cleared bug when being used<commit_after>var custom = (function () {
    var api = {
        apis: [],
        name: "custom",
        name_zh: "自定义",
        type: 'custom',
        chance: 0
    }

    var exported = {
        getImageUrl: getImageUrl,
        api: api
    }
    function getImageUrl() {
        // _randomPick will splice array, push element back to it
        var url = _randomPick(exported.api.apis, 1).shift();
        exported.api.apis.push(url);
        if (url) {
            return Promise.resolve(url);
        } else {
            return Promise.reject();
        }
    }
    return exported;
})();<|endoftext|>


<commit_before>/**
The enyo.Panels kind is designed to satisfy a variety of common application layout use cases.
For example, controls can be arranged as a carousel, a set of collapsing panels, a card stack
that fades between panels, a grid, and more.

Of the set of controls contained inside an enyo.Panels, one is considered active.
Any enyo kind can be placed inside an enyo.Panels but by convention we refer to each of these
controls as a "panel." The active panel is set by index using the setIndex method.

The layout of panels is controlled by the specified layoutKind. By default, panels fit to
the size of the Panels that contains them and transition via fading. To setup a carousel,
specify a layoutKind of "CarouselArranger" and give each panel a width.

Panels are also animate and are draggable by default. These behaviors can be defeated by
setting the draggable and animate properties to false.

Here's an example:

		enyo.kind({
			name: "App",
			kind: "Panels",
			fit: true,
			components: [
				{kind: "MyStartPanel"},
				{kind: "MyMiddlePanel"},
				{kind: "MyLastPanel"}
			]
		});
		new App().write();
*/
enyo.kind({
	name: "enyo.Panels",
	classes: "enyo-panels",
	published: {
		/**
			Specify the index of the active panel. The layout of panels is
			controlled by the layoutKind, but as a rule, the active panel is displayed
			in the most prominent position. In the default, CardArranger layout, for example,
			the active panel is shown and the other panels are hidden.
		*/
		index: 0,
		//* Controls if the user can drag between panels.
		draggable: true,
		//* Controls if the panels animate when transitioning; for example, when setIndex is called.
		animate: true,
		wrap: false
	},
	events: {
		/**
			Event that fires at the start of a panel transition.
			Note, this event fires when setIndex is called and also during dragging.
		*/
		onTransitionStart: "",
		/**
			Event that fires at the end of a panel transition.
			Note, this event fires when setIndex is called and also during dragging.
		*/
		onTransitionFinish: ""
	},
	//* @protected
	handlers: {
		ondragstart: "dragstart",
		ondrag: "drag",
		ondragfinish: "dragfinish"
	},
	tools: [
		{kind: "Animator", onStep: "step", onEnd: "completed"}
	],
	layoutKind: "CardArranger",
	fraction: 0,
	create: function() {
		this.transitionPoints = [];
		this.inherited(arguments);
		this.indexChanged();
	},
	initComponents: function() {
		this.createChrome(this.tools);
		this.inherited(arguments);
	},
	removeControl: function(inControl) {
		this.inherited(arguments);
		if (this.isPanel(inControl)) {
			this.flow();
			this.reflow();
			this.setIndex(0);
		}
	},
	isPanel: function() {
		return true;
	},
	flow: function() {
		this.arrangements = [];
		this.inherited(arguments);
	},
	reflow: function() {
		this.arrangements = [];
		this.inherited(arguments);
		this.refresh();
	},
	//* @public
	//* Return an array of contained panels.
	getPanels: function() {
		var p = this.controlParent || this;
		return p.children;
	},
	//* Return a reference to the active panel; the panel at the specified index.
	getActive: function() {
		var p$ = this.getPanels();
		return p$[this.index];
	},
	/**
		Return a reference to the enyo.Animator
		instance used to animate panel transitions. The Panels' animator can be used
		to, for example set the duration of panel transitions: e.g. this.getAnimator().setDuration(1000);
	*/
	getAnimator: function() {
		return this.$.animator;
	},
	/** Set the active panel to the panel specified by the given index.
	Note that if the animate property is set to true, the active panel
	will animate into view.
	*/
	setIndex: function(inIndex) {
		// override setIndex so that indexChanged is called
		// whether this.index has actually changed or not
		this.setPropertyValue("index", inIndex, "indexChanged");
	},
	/**
		Set the active panel to the panel specified by the given index.
		Regardless of the setting of the animate property, the transition to the next panel
		will not animate and will be immediate.
	*/
	setIndexDirect: function(inIndex) {
		this.setIndex(inIndex);
		this.completed();
	},
	//** Transition to the previous panel; the panel at the index one before the current active panel.
	previous: function() {
		this.setIndex(this.index-1);
	},
	//** Transition to the next panel; the panel at the index one after the current active panel.
	next: function() {
		this.setIndex(this.index+1);
	},
	//* @protected
	clamp: function(inValue) {
		var l = this.getPanels().length-1;
		if (this.wrap) {
			// FIXME: dragging makes assumptions about direction and from->start indexes.
			//return inValue < 0 ? l : (inValue > l ? 0 : inValue);
			return inValue;
		} else {
			return Math.max(0, Math.min(inValue, l));
		}
	},
	indexChanged: function(inOld) {
		this.lastIndex = inOld;
		this.index = this.clamp(this.index);
		if (!this.dragging) {
			if (this.$.animator.isAnimating()) {
				this.completed();
			}
			this.$.animator.stop();
			if (this.hasNode() && this.animate) {
				this.startTransition();
				this.$.animator.play({
					startValue: this.fraction
				});
			} else {
				this.refresh();
			}
		}
	},
	step: function(inSender) {
		this.fraction = inSender.value;
		this.stepTransition();
	},
	completed: function() {
		if (this.$.animator.isAnimating()) {
			this.$.animator.stop();
		}
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	dragstart: function(inSender, inEvent) {
		if (this.draggable && this.layout.canDragEvent(inEvent)) {
			inEvent.preventDefault();
			this.dragstartTransition(inEvent);
			this.dragging = true;
			this.$.animator.stop();
			return true;
		}
	},
	drag: function(inSender, inEvent) {
		if (this.dragging) {
			inEvent.preventDefault();
			this.dragTransition(inEvent);
		}
	},
	dragfinish: function(inSender, inEvent) {
		if (this.dragging) {
			this.dragging = false;
			inEvent.preventTap();
			this.dragfinishTransition(inEvent);
		}
	},
	dragstartTransition: function(inEvent) {
		if (!this.$.animator.isAnimating()) {
			var f = this.fromIndex = this.index;
			this.toIndex = f - this.layout.calcDragDirection(inEvent);
		} else {
			this.verifyDragTransition(inEvent);
		}
		this.fromIndex = this.clamp(this.fromIndex);
		this.toIndex = this.clamp(this.toIndex);
		//this.log(this.fromIndex, this.toIndex);
		this.fireTransitionStart();
		this.layout.start();
	},
	dragTransition: function(inEvent) {
		// note: for simplicity we choose to calculate the distance directly between
		// the first and last transition point.
		var d = this.layout.calcDrag(inEvent);
		var t$ = this.transitionPoints, s = t$[0], f = t$[t$.length-1];
		var as = this.fetchArrangement(s);
		var af = this.fetchArrangement(f);
		var dx = this.layout.drag(d, s, as, f, af);
		var dragFail = d && !dx;
		if (dragFail) {
			//this.log(dx, s, as, f, af);
		}
		this.fraction += dx;
		var f = this.fraction;
		if (f > 1 || f < 0 || dragFail) {
			if (f > 0 || dragFail) {
				this.dragfinishTransition(inEvent);
			}
			this.dragstartTransition(inEvent);
			this.fraction = 0;
			// FIXME: account for lost fraction
			//this.dragTransition(inEvent);
		}
		this.stepTransition();
	},
	dragfinishTransition: function(inEvent) {
		this.verifyDragTransition(inEvent);
		this.setIndex(this.toIndex);
	},
	verifyDragTransition: function(inEvent) {
		var d = this.layout.calcDragDirection(inEvent);
		var f = Math.min(this.fromIndex, this.toIndex);
		var t = Math.max(this.fromIndex, this.toIndex);
		if (d > 0) {
			var s = f;
			f = t;
			t = s;
		}
		if (f != this.fromIndex) {
			this.fraction = 1 - this.fraction;
		}
		//this.log("old", this.fromIndex, this.toIndex, "new", f, t);
		this.fromIndex = f;
		this.toIndex = t;
	},
	refresh: function() {
		this.startTransition()
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	startTransition: function() {
		this.fromIndex = this.fromIndex != null ? this.fromIndex : this.lastIndex || 0;
		this.toIndex = this.toIndex != null ? this.toIndex : this.index;
		//this.log(this.id, this.fromIndex, this.toIndex);
		this.layout.start();
		this.fireTransitionStart();
	},
	finishTransition: function() {
		this.layout.finish();
		this.transitionPoints = [];
		this.fraction = 0;
		this.fromIndex = this.toIndex = null;
	},
	fireTransitionStart: function() {
		if (this.hasNode()) {
			this.doTransitionStart({fromIndex: this.fromIndex, toIndex: this.toIndex});
		}
	},
	fireTransitionFinish: function() {
		if (this.hasNode()) {
			this.doTransitionFinish({fromIndex: this.lastIndex, toIndex: this.index});
		}
	},
	// gambit: we interpolate between arrangements as needed.
	stepTransition: function() {
		if (this.hasNode()) {
			// select correct transition points and normalize fraction.
			var t$ = this.transitionPoints;
			var r = (this.fraction || 0) * (t$.length-1);
			var i = Math.floor(r);
			r = r - i;
			var s = t$[i], f = t$[i+1];
			// get arrangements and lerp between them
			var s0 = this.fetchArrangement(s);
			var s1 = this.fetchArrangement(f);
			this.arrangement = s0 && s1 ? enyo.Panels.lerp(s0, s1, r) : (s0 || s1);
			if (this.arrangement) {
				this.layout.flowArrangement();
			}
		}
	},
	fetchArrangement: function(inName) {
		if ((inName != null) && !this.arrangements[inName]) {
			this.layout._arrange(inName);
			this.arrangements[inName] = this.readArrangement(this.children);
		}
		return this.arrangements[inName];
	},
	readArrangement: function(inC) {
		var r = [];
		for (var i=0, c$=inC, c; c=c$[i]; i++) {
			r.push(enyo.clone(c._arranger));
		}
		return r;
	},
	statics: {
		lerp: function(inA0, inA1, inFrac) {
			var r = [];
			for (var i=0, k$=enyo.keys(inA0), k; k=k$[i]; i++) {
				r.push(this.lerpObject(inA0[k], inA1[k], inFrac));
			}
			return r;
		},
		lerpObject: function(inNew, inOld, inFrac) {
			var b = enyo.clone(inNew), n, o;
			for (var i in inNew) {
				n = inNew[i];
				o =  inOld[i];
				if (n != o) {
					b[i] = n - (n - o) * inFrac;
				}
			}
			return b;
		}
	}
});
<commit_msg>Panels: call fireTransitionFinish in finishTransition<commit_after>/**
The enyo.Panels kind is designed to satisfy a variety of common application layout use cases.
For example, controls can be arranged as a carousel, a set of collapsing panels, a card stack
that fades between panels, a grid, and more.

Of the set of controls contained inside an enyo.Panels, one is considered active.
Any enyo kind can be placed inside an enyo.Panels but by convention we refer to each of these
controls as a "panel." The active panel is set by index using the setIndex method.

The layout of panels is controlled by the specified layoutKind. By default, panels fit to
the size of the Panels that contains them and transition via fading. To setup a carousel,
specify a layoutKind of "CarouselArranger" and give each panel a width.

Panels are also animate and are draggable by default. These behaviors can be defeated by
setting the draggable and animate properties to false.

Here's an example:

		enyo.kind({
			name: "App",
			kind: "Panels",
			fit: true,
			components: [
				{kind: "MyStartPanel"},
				{kind: "MyMiddlePanel"},
				{kind: "MyLastPanel"}
			]
		});
		new App().write();
*/
enyo.kind({
	name: "enyo.Panels",
	classes: "enyo-panels",
	published: {
		/**
			Specify the index of the active panel. The layout of panels is
			controlled by the layoutKind, but as a rule, the active panel is displayed
			in the most prominent position. In the default, CardArranger layout, for example,
			the active panel is shown and the other panels are hidden.
		*/
		index: 0,
		//* Controls if the user can drag between panels.
		draggable: true,
		//* Controls if the panels animate when transitioning; for example, when setIndex is called.
		animate: true,
		wrap: false
	},
	events: {
		/**
			Event that fires at the start of a panel transition.
			Note, this event fires when setIndex is called and also during dragging.
		*/
		onTransitionStart: "",
		/**
			Event that fires at the end of a panel transition.
			Note, this event fires when setIndex is called and also during dragging.
		*/
		onTransitionFinish: ""
	},
	//* @protected
	handlers: {
		ondragstart: "dragstart",
		ondrag: "drag",
		ondragfinish: "dragfinish"
	},
	tools: [
		{kind: "Animator", onStep: "step", onEnd: "completed"}
	],
	layoutKind: "CardArranger",
	fraction: 0,
	create: function() {
		this.transitionPoints = [];
		this.inherited(arguments);
		this.indexChanged();
	},
	initComponents: function() {
		this.createChrome(this.tools);
		this.inherited(arguments);
	},
	removeControl: function(inControl) {
		this.inherited(arguments);
		if (this.isPanel(inControl)) {
			this.flow();
			this.reflow();
			this.setIndex(0);
		}
	},
	isPanel: function() {
		return true;
	},
	flow: function() {
		this.arrangements = [];
		this.inherited(arguments);
	},
	reflow: function() {
		this.arrangements = [];
		this.inherited(arguments);
		this.refresh();
	},
	//* @public
	//* Return an array of contained panels.
	getPanels: function() {
		var p = this.controlParent || this;
		return p.children;
	},
	//* Return a reference to the active panel; the panel at the specified index.
	getActive: function() {
		var p$ = this.getPanels();
		return p$[this.index];
	},
	/**
		Return a reference to the enyo.Animator
		instance used to animate panel transitions. The Panels' animator can be used
		to, for example set the duration of panel transitions: e.g. this.getAnimator().setDuration(1000);
	*/
	getAnimator: function() {
		return this.$.animator;
	},
	/** Set the active panel to the panel specified by the given index.
	Note that if the animate property is set to true, the active panel
	will animate into view.
	*/
	setIndex: function(inIndex) {
		// override setIndex so that indexChanged is called
		// whether this.index has actually changed or not
		this.setPropertyValue("index", inIndex, "indexChanged");
	},
	/**
		Set the active panel to the panel specified by the given index.
		Regardless of the setting of the animate property, the transition to the next panel
		will not animate and will be immediate.
	*/
	setIndexDirect: function(inIndex) {
		this.setIndex(inIndex);
		this.completed();
	},
	//** Transition to the previous panel; the panel at the index one before the current active panel.
	previous: function() {
		this.setIndex(this.index-1);
	},
	//** Transition to the next panel; the panel at the index one after the current active panel.
	next: function() {
		this.setIndex(this.index+1);
	},
	//* @protected
	clamp: function(inValue) {
		var l = this.getPanels().length-1;
		if (this.wrap) {
			// FIXME: dragging makes assumptions about direction and from->start indexes.
			//return inValue < 0 ? l : (inValue > l ? 0 : inValue);
			return inValue;
		} else {
			return Math.max(0, Math.min(inValue, l));
		}
	},
	indexChanged: function(inOld) {
		this.lastIndex = inOld;
		this.index = this.clamp(this.index);
		if (!this.dragging) {
			if (this.$.animator.isAnimating()) {
				this.completed();
			}
			this.$.animator.stop();
			if (this.hasNode() && this.animate) {
				this.startTransition();
				this.$.animator.play({
					startValue: this.fraction
				});
			} else {
				this.refresh();
			}
		}
	},
	step: function(inSender) {
		this.fraction = inSender.value;
		this.stepTransition();
	},
	completed: function() {
		if (this.$.animator.isAnimating()) {
			this.$.animator.stop();
		}
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	dragstart: function(inSender, inEvent) {
		if (this.draggable && this.layout.canDragEvent(inEvent)) {
			inEvent.preventDefault();
			this.dragstartTransition(inEvent);
			this.dragging = true;
			this.$.animator.stop();
			return true;
		}
	},
	drag: function(inSender, inEvent) {
		if (this.dragging) {
			inEvent.preventDefault();
			this.dragTransition(inEvent);
		}
	},
	dragfinish: function(inSender, inEvent) {
		if (this.dragging) {
			this.dragging = false;
			inEvent.preventTap();
			this.dragfinishTransition(inEvent);
		}
	},
	dragstartTransition: function(inEvent) {
		if (!this.$.animator.isAnimating()) {
			var f = this.fromIndex = this.index;
			this.toIndex = f - this.layout.calcDragDirection(inEvent);
		} else {
			this.verifyDragTransition(inEvent);
		}
		this.fromIndex = this.clamp(this.fromIndex);
		this.toIndex = this.clamp(this.toIndex);
		//this.log(this.fromIndex, this.toIndex);
		this.fireTransitionStart();
		this.layout.start();
	},
	dragTransition: function(inEvent) {
		// note: for simplicity we choose to calculate the distance directly between
		// the first and last transition point.
		var d = this.layout.calcDrag(inEvent);
		var t$ = this.transitionPoints, s = t$[0], f = t$[t$.length-1];
		var as = this.fetchArrangement(s);
		var af = this.fetchArrangement(f);
		var dx = this.layout.drag(d, s, as, f, af);
		var dragFail = d && !dx;
		if (dragFail) {
			//this.log(dx, s, as, f, af);
		}
		this.fraction += dx;
		var f = this.fraction;
		if (f > 1 || f < 0 || dragFail) {
			if (f > 0 || dragFail) {
				this.dragfinishTransition(inEvent);
			}
			this.dragstartTransition(inEvent);
			this.fraction = 0;
			// FIXME: account for lost fraction
			//this.dragTransition(inEvent);
		}
		this.stepTransition();
	},
	dragfinishTransition: function(inEvent) {
		this.verifyDragTransition(inEvent);
		this.setIndex(this.toIndex);
	},
	verifyDragTransition: function(inEvent) {
		var d = this.layout.calcDragDirection(inEvent);
		var f = Math.min(this.fromIndex, this.toIndex);
		var t = Math.max(this.fromIndex, this.toIndex);
		if (d > 0) {
			var s = f;
			f = t;
			t = s;
		}
		if (f != this.fromIndex) {
			this.fraction = 1 - this.fraction;
		}
		//this.log("old", this.fromIndex, this.toIndex, "new", f, t);
		this.fromIndex = f;
		this.toIndex = t;
	},
	refresh: function() {
		this.startTransition()
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	startTransition: function() {
		this.fromIndex = this.fromIndex != null ? this.fromIndex : this.lastIndex || 0;
		this.toIndex = this.toIndex != null ? this.toIndex : this.index;
		//this.log(this.id, this.fromIndex, this.toIndex);
		this.layout.start();
		this.fireTransitionStart();
	},
	finishTransition: function() {
		this.layout.finish();
		this.transitionPoints = [];
		this.fraction = 0;
		this.fromIndex = this.toIndex = null;
		this.fireTransitionFinish();
	},
	fireTransitionStart: function() {
		if (this.hasNode()) {
			this.doTransitionStart({fromIndex: this.fromIndex, toIndex: this.toIndex});
		}
	},
	fireTransitionFinish: function() {
		if (this.hasNode()) {
			this.doTransitionFinish({fromIndex: this.lastIndex, toIndex: this.index});
		}
	},
	// gambit: we interpolate between arrangements as needed.
	stepTransition: function() {
		if (this.hasNode()) {
			// select correct transition points and normalize fraction.
			var t$ = this.transitionPoints;
			var r = (this.fraction || 0) * (t$.length-1);
			var i = Math.floor(r);
			r = r - i;
			var s = t$[i], f = t$[i+1];
			// get arrangements and lerp between them
			var s0 = this.fetchArrangement(s);
			var s1 = this.fetchArrangement(f);
			this.arrangement = s0 && s1 ? enyo.Panels.lerp(s0, s1, r) : (s0 || s1);
			if (this.arrangement) {
				this.layout.flowArrangement();
			}
		}
	},
	fetchArrangement: function(inName) {
		if ((inName != null) && !this.arrangements[inName]) {
			this.layout._arrange(inName);
			this.arrangements[inName] = this.readArrangement(this.children);
		}
		return this.arrangements[inName];
	},
	readArrangement: function(inC) {
		var r = [];
		for (var i=0, c$=inC, c; c=c$[i]; i++) {
			r.push(enyo.clone(c._arranger));
		}
		return r;
	},
	statics: {
		lerp: function(inA0, inA1, inFrac) {
			var r = [];
			for (var i=0, k$=enyo.keys(inA0), k; k=k$[i]; i++) {
				r.push(this.lerpObject(inA0[k], inA1[k], inFrac));
			}
			return r;
		},
		lerpObject: function(inNew, inOld, inFrac) {
			var b = enyo.clone(inNew), n, o;
			for (var i in inNew) {
				n = inNew[i];
				o =  inOld[i];
				if (n != o) {
					b[i] = n - (n - o) * inFrac;
				}
			}
			return b;
		}
	}
});
<|endoftext|>