diff --git a/avRegistration/auth-method-service.js b/avRegistration/auth-method-service.js index 4816617a..2d8736bb 100644 --- a/avRegistration/auth-method-service.js +++ b/avRegistration/auth-method-service.js @@ -156,6 +156,17 @@ angular.module('avRegistration') return $http.post(url, params); }; + /** + * @returns the http request + */ + authmethod.obtainVoterAuthCode = function (electionId, username) + { + var params = {username: username}; + var url = backendUrl + 'auth-event/' + electionId + '/generate-auth-code/'; + + return $http.post(url, params); + }; + /** * @returns the http request */ @@ -368,6 +379,25 @@ angular.module('avRegistration') return fields; }; + authmethod.getLoginWithCode = function (_viewEventData) { + return [ + { + "name": "__username", + "type": "text", + "required": true, + "min": 3, + "max": 200, + "required_on_authentication": true + }, + { + "name": "code", + "type": "code", + "required": true, + "required_on_authentication": true + } + ]; + }; + authmethod.getLoginFields = function (viewEventData) { var fields = authmethod.getRegisterFields( viewEventData diff --git a/avRegistration/fields/text-field-directive/text-field-directive.html b/avRegistration/fields/text-field-directive/text-field-directive.html index 596dcea3..55c30516 100644 --- a/avRegistration/fields/text-field-directive/text-field-directive.html +++ b/avRegistration/fields/text-field-directive/text-field-directive.html @@ -3,8 +3,15 @@ class="form-group" ng-class="{'has-error': fieldForm.input.$dirty && fieldForm.input.$invalid}">
+ with-code="{{withCode}}" + username="{{username}}" + ng-if="!isOpenId" + >
-if (angular.module("avRegistration", [ "ui.bootstrap", "ui.utils", "ui.router" ]), -angular.module("avRegistration").config(function() {}), angular.module("avRegistration").factory("Authmethod", [ "$http", "$cookies", "ConfigService", "$interval", "$location", function($http, $cookies, ConfigService, $interval, $location) { - var backendUrl = ConfigService.authAPI, authId = ConfigService.freeAuthId, authmethod = { - captcha_code: null, - captcha_image_url: "", - captcha_status: "", - admin: !1, - getAuthevent: function() { - var adminId = ConfigService.freeAuthId + "", electionsMatch = $location.path(), authevent = "", adminMatch = electionsMatch.match(/^\/admin\//), boothMatch = electionsMatch.match(/^\/booth\/([0-9]+)\//), electionsMatch = electionsMatch.match(/^\/(elections|election)\/([0-9]+)\//); - return _.isArray(adminMatch) ? authevent = adminId : _.isArray(boothMatch) && 2 === boothMatch.length ? authevent = boothMatch[1] : _.isArray(electionsMatch) && 3 === electionsMatch.length && (authevent = electionsMatch[2]), - authevent; - }, - isAdmin: function() { + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration', ['ui.bootstrap','ui.utils','ui.router']); + +angular.module('avRegistration').config(function() { + /* Add New States Above */ +}); +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + + .factory('Authmethod', function($http, $cookies, ConfigService, $interval, $location) { + var backendUrl = ConfigService.authAPI; + var authId = ConfigService.freeAuthId; + var authmethod = {}; + authmethod.captcha_code = null; + authmethod.captcha_image_url = ""; + authmethod.captcha_status = ""; + authmethod.admin = false; + + authmethod.getAuthevent = function() { + var adminId = ConfigService.freeAuthId + ''; + var href = $location.path(); + var authevent = ''; + + var adminMatch = href.match(/^\/admin\//); + var boothMatch = href.match(/^\/booth\/([0-9]+)\//); + var electionsMatch = href.match(/^\/(elections|election)\/([0-9]+)\//); + + if (_.isArray(adminMatch)) { + authevent = adminId; + } else if(_.isArray(boothMatch) && 2 === boothMatch.length) { + authevent = boothMatch[1]; + } else if(_.isArray(electionsMatch) && 3 === electionsMatch.length) { + authevent = electionsMatch[2]; + } + return authevent; + }; + + authmethod.isAdmin = function() { return authmethod.isLoggedIn() && authmethod.admin; - }, - isLoggedIn: function() { + }; + + authmethod.isLoggedIn = function() { var auth = $http.defaults.headers.common.Authorization; - return auth && 0 < auth.length; - }, - signup: function(data, eid) { - eid = eid || authId; - return $http.post(backendUrl + "auth-event/" + eid + "/register/", data); - }, - getUserInfoExtra: function() { - if (authmethod.isLoggedIn()) return $http.get(backendUrl + "user/extra/", {}); - var data = { - then: function(onSuccess, onError) { - return setTimeout(function() { - onError({ - data: { - message: "not-logged-in" - } - }); - }, 0), data; + return auth && auth.length > 0; + }; + + authmethod.signup = function(data, authevent) { + var eid = authevent || authId; + return $http.post(backendUrl + 'auth-event/'+eid+'/register/', data); + }; + + authmethod.getUserInfoExtra = function() { + if (!authmethod.isLoggedIn()) { + var data = { + then: function (onSuccess, onError) { + setTimeout(function() { + onError({data: {message:"not-logged-in"}}); + }, 0); + return data; } - }; - return data; - }, - getActivity: function(url, page, size, filterOptions, filterStr, receiver_id) { - var params = {}, url = backendUrl + "auth-event/" + url + "/activity/"; - return "max" === size ? params.size = 500 : angular.isNumber(size) && 0 < size && size < 500 ? params.size = parseInt(size) : params.size = 10, - angular.isNumber(page) ? params.page = parseInt(page) : params.page = 1, angular.isNumber(receiver_id) && (params.receiver_id = receiver_id), - _.extend(params, filterOptions), filterStr && 0 < filterStr.length && (params.filter = filterStr), - $http.get(url, { - params: params - }); - }, - getBallotBoxes: function(url, page, size, filterOptions, filterStr) { - var params = {}, url = backendUrl + "auth-event/" + url + "/ballot-box/"; - return "max" === size ? params.size = 500 : angular.isNumber(size) && 0 < size && size < 500 ? params.size = parseInt(size) : params.size = 10, - angular.isNumber(page) ? params.page = parseInt(page) : params.page = 1, _.extend(params, filterOptions), - filterStr && 0 < filterStr.length && (params.filter = filterStr), $http.get(url, { - params: params - }); - }, - createBallotBox: function(url, params) { - params = { - name: params - }, url = backendUrl + "auth-event/" + url + "/ballot-box/"; + }; + return data; + } + return $http.get(backendUrl + 'user/extra/', {}); + }; + + /** + * @returns an activity page + */ + authmethod.getActivity = function(eid, page, size, filterOptions, filterStr, receiver_id) + { + var params = {}; + var url = backendUrl + 'auth-event/' + eid + '/activity/'; + + // 1. initialize GET params + + if (size === 'max') { + params.size = 500; + } else if (angular.isNumber(size) && size > 0 && size < 500) { + params.size = parseInt(size); + } else { + params.size = 10; + } + + if (!angular.isNumber(page)) { + params.page = 1; + } else { + params.page = parseInt(page); + } + + + if (angular.isNumber(receiver_id)) { + params.receiver_id = receiver_id; + } + + _.extend(params, filterOptions); + if (filterStr && filterStr.length > 0) { + params.filter = filterStr; + } + + // 2. generate request + return $http.get(url, {params: params}); + }; + + /** + * @returns a page of ballot boxes + */ + authmethod.getBallotBoxes = function(eid, page, size, filterOptions, filterStr) + { + var params = {}; + var url = backendUrl + 'auth-event/' + eid + '/ballot-box/'; + + // 1. initialize GET params + + if (size === 'max') { + params.size = 500; + } else if (angular.isNumber(size) && size > 0 && size < 500) { + params.size = parseInt(size); + } else { + params.size = 10; + } + + if (!angular.isNumber(page)) { + params.page = 1; + } else { + params.page = parseInt(page); + } + + _.extend(params, filterOptions); + if (filterStr && filterStr.length > 0) { + params.filter = filterStr; + } + + // 2. generate request + return $http.get(url, {params: params}); + }; + + /** + * @returns the http request + */ + authmethod.createBallotBox = function(eid, name) + { + var params = {name: name}; + var url = backendUrl + 'auth-event/' + eid + '/ballot-box/'; + return $http.post(url, params); - }, - postTallySheet: function(eid, url, data) { - url = backendUrl + "auth-event/" + eid + "/ballot-box/" + url + "/tally-sheet/"; + }; + + /** + * @returns the http request + */ + authmethod.obtainVoterAuthCode = function (electionId, username) + { + var params = {username: username}; + var url = backendUrl + 'auth-event/' + electionId + '/generate-auth-code/'; + + return $http.post(url, params); + }; + + /** + * @returns the http request + */ + authmethod.postTallySheet = function(eid, ballot_box_id, data) + { + var url = backendUrl + 'auth-event/' + eid + '/ballot-box/' + ballot_box_id + '/tally-sheet/'; + return $http.post(url, data); - }, - voteStats: function(url) { - url = backendUrl + "auth-event/" + url + "/vote-stats/"; + }; + + /** + * @returns the http request + */ + authmethod.voteStats = function(eid) + { + var url = backendUrl + 'auth-event/' + eid + '/vote-stats/'; + return $http.get(url); - }, - getTallySheet: function(eid, ballot_box_id, tally_sheet_id) { - var url = null, url = tally_sheet_id ? backendUrl + "auth-event/" + eid + "/ballot-box/" + ballot_box_id + "/tally-sheet/" + tally_sheet_id + "/" : backendUrl + "auth-event/" + eid + "/ballot-box/" + ballot_box_id + "/tally-sheet/"; + }; + + + /** + * @returns the http request + */ + authmethod.getTallySheet = function(eid, ballot_box_id, tally_sheet_id) + { + var url = null; + if (!tally_sheet_id) { + url = backendUrl + 'auth-event/' + eid + '/ballot-box/' + ballot_box_id + '/tally-sheet/'; + } else { + url = backendUrl + 'auth-event/' + eid + '/ballot-box/' + ballot_box_id + '/tally-sheet/' + tally_sheet_id + '/'; + } + return $http.get(url); - }, - deleteTallySheet: function(eid, ballot_box_id, url) { - url = backendUrl + "auth-event/" + eid + "/ballot-box/" + ballot_box_id + "/tally-sheet/" + url + "/"; + }; + + /** + * @returns the http request + */ + authmethod.deleteTallySheet = function(eid, ballot_box_id, tally_sheet_id) + { + var url = backendUrl + 'auth-event/' + eid + '/ballot-box/' + ballot_box_id + '/tally-sheet/' + tally_sheet_id + "/"; + return $http.delete(url, {}); - }, - deleteBallotBox: function(eid, url) { - url = backendUrl + "auth-event/" + eid + "/ballot-box/" + url + "/delete/"; + }; + + /** + * @returns the http request + */ + authmethod.deleteBallotBox = function(eid, ballot_box_id) + { + var url = backendUrl + 'auth-event/' + eid + '/ballot-box/' + ballot_box_id + "/delete/"; + return $http.delete(url, {}); - }, - updateUserExtra: function(extra) { - if (authmethod.isLoggedIn()) return $http.post(backendUrl + "user/extra/", extra); - var data = { - then: function(onSuccess, onError) { - return setTimeout(function() { - onError({ - data: { - message: "not-logged-in" - } - }); - }, 0), data; + }; + + authmethod.updateUserExtra = function (extra) { + if (!authmethod.isLoggedIn()) { + var data = { + then: function (onSuccess, onError) { + setTimeout(function() { + onError({data: {message:"not-logged-in"}}); + }, 0); + return data; } - }; - return data; - }, - getUserInfo: function(userid) { - if (authmethod.isLoggedIn()) return void 0 === userid ? $http.get(backendUrl + "user/", {}) : $http.get(backendUrl + "user/%d" % userid, {}); - var data = { - then: function(onSuccess, onError) { - return setTimeout(function() { - onError({ - data: { - message: "not-logged-in" - } - }); - }, 0), data; + }; + return data; + } + return $http.post(backendUrl + 'user/extra/', extra); + }; + + authmethod.getUserInfo = function(userid) { + if (!authmethod.isLoggedIn()) { + var data = { + then: function (onSuccess, onError) { + setTimeout(function() { + onError({data: {message:"not-logged-in"}}); + }, 0); + return data; } - }; - return data; - }, - ping: function() { - if (authmethod.isLoggedIn()) return $http.get(backendUrl + "auth-event/" + authId + "/ping/"); - var data = { - then: function(onSuccess, onError) { - return setTimeout(function() { - onError({ - data: { - message: "not-logged-in" - } - }); - }, 0), data; + }; + return data; + } + if (typeof userid === 'undefined') { + return $http.get(backendUrl + 'user/', {}); + } else { + return $http.get(backendUrl + 'user/%d' % userid, {}); + } + }; + + authmethod.ping = function() { + if (!authmethod.isLoggedIn()) { + var data = { + then: function (onSuccess, onError) { + setTimeout(function() { + onError({data: {message:"not-logged-in"}}); + }, 0); + return data; } - }; - return data; - }, - getImage: function(ev, uid) { - return $http.get(backendUrl + "auth-event/" + ev + "/census/img/" + uid + "/"); - }, - login: function(data, eid) { - eid = eid || authId; - return delete data.authevent, $http.post(backendUrl + "auth-event/" + eid + "/authenticate/", data); - }, - censusQuery: function(data, eid) { - eid = eid || authId; - return delete data.authevent, $http.post(backendUrl + "auth-event/" + eid + "/census/public-query/", data); - }, - resendAuthCode: function(data, eid) { - return $http.post(backendUrl + "auth-event/" + eid + "/resend_auth_code/", data); - }, - editChildrenParent: function(data, eid) { - return $http.post(backendUrl + "auth-event/" + eid + "/edit-children-parent/", data); - }, - getPerm: function(perm, object_type, data) { - data = { + }; + return data; + } + return $http.get(backendUrl + 'auth-event/'+authId+'/ping/'); + }; + + authmethod.getImage = function(ev, uid) { + return $http.get(backendUrl + 'auth-event/'+ev+'/census/img/'+uid+'/'); + }; + + authmethod.login = function(data, authevent) { + var eid = authevent || authId; + delete data['authevent']; + return $http.post(backendUrl + 'auth-event/'+eid+'/authenticate/', data); + }; + + authmethod.censusQuery = function(data, authevent) { + var eid = authevent || authId; + delete data['authevent']; + return $http.post(backendUrl + 'auth-event/'+eid+'/census/public-query/', data); + }; + + authmethod.resendAuthCode = function(data, eid) { + return $http.post(backendUrl + 'auth-event/'+eid+'/resend_auth_code/', data); + }; + + authmethod.editChildrenParent = function(data, eid) { + return $http.post(backendUrl + 'auth-event/'+eid+'/edit-children-parent/', data); + }; + + authmethod.getPerm = function(perm, object_type, object_id) { + var data = { permission: perm, object_type: object_type, - object_id: null === data ? data : data + "" + object_id: (object_id === null) ? object_id : object_id + "" // to convert to string }; - return $http.post(backendUrl + "get-perms/", data); - }, - viewEvent: function(id) { - return $http.get(backendUrl + "auth-event/" + id + "/"); - }, - viewEvents: function() { - return $http.get(backendUrl + "auth-event/"); - }, - createEvent: function(data) { - return $http.post(backendUrl + "auth-event/", data); - }, - editEvent: function(id, data) { - return $http.post(backendUrl + "auth-event/" + id + "/", data); - }, - addCensus: function(id, d, validation) { - d = { - "field-validation": validation = !angular.isDefined(validation) ? "enabled" : validation, - census: d + return $http.post(backendUrl + 'get-perms/', data); + }; + + authmethod.viewEvent = function(id) { + return $http.get(backendUrl + 'auth-event/' + id + '/'); + }; + + authmethod.viewEvents = function() { + return $http.get(backendUrl + 'auth-event/'); + }; + + authmethod.createEvent = function(data) { + return $http.post(backendUrl + 'auth-event/', data); + }; + + authmethod.editEvent = function(id, data) { + return $http.post(backendUrl + 'auth-event/' + id +'/', data); + }; + + authmethod.addCensus = function(id, data, validation) { + if (!angular.isDefined(validation)) { + validation = "enabled"; + } + var d = { + "field-validation": validation, + "census": data }; - return $http.post(backendUrl + "auth-event/" + id + "/census/", d); - }, - getCensus: function(id, params) { - return angular.isObject(params) ? $http.get(backendUrl + "auth-event/" + id + "/census/", { - params: params - }) : $http.get(backendUrl + "auth-event/" + id + "/census/"); - }, - getRegisterFields: function(viewEventData) { - for (var fields = (fields = _.filter(angular.copy(viewEventData.extra_fields), function(item) { - return !0 !== item.required_when_registered; - })) || [], i = 0; i < fields.length; i++) if ("captcha" === fields[i].type) { - var captcha = fields.splice(i, 1); - fields.push(captcha[0]); - break; + return $http.post(backendUrl + 'auth-event/' + id + '/census/', d); + }; + + authmethod.getCensus = function(id, params) { + if (!angular.isObject(params)) { + return $http.get(backendUrl + 'auth-event/' + id + '/census/'); + } + + return $http.get( + backendUrl + 'auth-event/' + id + '/census/', + {params:params}); + }; + + authmethod.getRegisterFields = function (viewEventData) { + var fields = _.filter( + angular.copy(viewEventData.extra_fields), + function (item) { + if (true === item.required_when_registered) { + return false; + } + return true; + }); + + if (!fields) { fields = []; } + + // put captcha the last + for (var i = 0; i < fields.length; i++) { + if (fields[i]['type'] === "captcha") { + var captcha = fields.splice(i, 1); + fields.push(captcha[0]); + break; } + } + return fields; + }; + + authmethod.getCensusQueryFields = function (viewEventData) + { + var fields = angular.copy(viewEventData.extra_fields); + + fields = _.filter( + fields, + function (field) { + return field.required_on_authentication; + } + ); + return fields; - }, - getCensusQueryFields: function(fields) { - fields = angular.copy(fields.extra_fields); - return fields = _.filter(fields, function(field) { - return field.required_on_authentication; - }); - }, - getLoginFields: function(viewEventData) { - var fields = authmethod.getRegisterFields(viewEventData); - _.contains([ "sms", "email" ], viewEventData.auth_method) ? fields.push({ - name: "code", - type: "code", - required: !0, - required_on_authentication: !0 - }) : _.contains([ "sms-otp", "email-otp" ], viewEventData.auth_method) && fields.push({ - name: "code", - type: "code", - required: !0, - steps: [ 1 ], - required_on_authentication: !0 - }), fields = _.filter(fields, function(field) { - return field.required_on_authentication; - }); - for (var i = 0; i < fields.length; i++) if ("captcha" === fields[i].type) { - var captcha = fields.splice(i, 1); - fields.push(captcha[0]); - break; + }; + + authmethod.getLoginWithCode = function (_viewEventData) { + return [ + { + "name": "__username", + "type": "text", + "required": true, + "min": 3, + "max": 200, + "required_on_authentication": true + }, + { + "name": "code", + "type": "code", + "required": true, + "required_on_authentication": true + } + ]; + }; + + authmethod.getLoginFields = function (viewEventData) { + var fields = authmethod.getRegisterFields( + viewEventData + ); + if (_.contains(["sms", "email"], viewEventData.auth_method)) + { + fields.push({ + "name": "code", + "type": "code", + "required": true, + "required_on_authentication": true + }); + } else if (_.contains(["sms-otp", "email-otp"], viewEventData.auth_method)) + { + fields.push({ + "name": "code", + "type": "code", + "required": true, + "steps": [1], + "required_on_authentication": true + }); + } + + fields = _.filter(fields, function (field) {return field.required_on_authentication;}); + + // put captha the last + for (var i=0; i + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .controller( + 'LoginController', + function( + $scope, + $stateParams + ) { + $scope.event_id = $stateParams.id; + $scope.code = $stateParams.code; + $scope.email = $stateParams.email; + $scope.username = $stateParams.username; + $scope.isOpenId = $stateParams.isOpenId; + $scope.withCode = $stateParams.withCode; + } + ); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive( + 'avLogin', + function( + Authmethod, + StateDataService, + $state, + $location, + $cookies, + $i18next, + $window, + $timeout, + ConfigService, + Patterns) + { + // we use it as something similar to a controller here + function link(scope, element, attrs) + { + scope.isCensusQuery = attrs.isCensusQuery; + scope.withCode = attrs.withCode; + scope.username = attrs.username; + scope.error = null; + + // by default + scope.hide_default_login_lookup_field = false; + var adminId = ConfigService.freeAuthId + ''; + var autheventid = scope.eventId = attrs.eventId; + scope.orgName = ConfigService.organization.orgName; + scope.openIDConnectProviders = ConfigService.openIDConnectProviders; + + // redirect from admin login to admin elections if login is not needed + var autheventCookie = $cookies.get('authevent_' + adminId); + var authCookie = $cookies.get('auth_authevent_' + adminId); + if (!!autheventCookie && autheventCookie === adminId && + autheventid === adminId && !!authCookie) + { + $window.location.href = '/admin/elections'; } - }; - return authmethod; -} ]), angular.module("avRegistration").controller("LoginController", [ "$scope", "$stateParams", "$filter", "$i18next", "$cookies", "$window", "ConfigService", "Authmethod", function($scope, $stateParams, $filter, $i18next, $cookies, $window, ConfigService, Authmethod) { - $scope.event_id = $stateParams.id, $scope.code = $stateParams.code, $scope.email = $stateParams.email, - $scope.isOpenId = $stateParams.isOpenId; -} ]), angular.module("avRegistration").directive("avLogin", [ "Authmethod", "StateDataService", "$state", "$location", "$cookies", "$i18next", "$window", "$timeout", "ConfigService", "Patterns", function(Authmethod, StateDataService, $state, $location, $cookies, $i18next, $window, $timeout, ConfigService, Patterns) { - return { - restrict: "AE", - scope: !0, - link: function(scope, element, attrs) { - scope.isCensusQuery = attrs.isCensusQuery, scope.error = null, scope.hide_default_login_lookup_field = !1; - var adminId = ConfigService.freeAuthId + "", autheventid = scope.eventId = attrs.eventId; - scope.orgName = ConfigService.organization.orgName, scope.openIDConnectProviders = ConfigService.openIDConnectProviders; - var autheventCookie = $cookies.get("authevent_" + adminId), authCookie = $cookies.get("auth_authevent_" + adminId); - function randomStr() { - var random = sjcl.random.randomWords(64, 0); - return sjcl.codec.hex.fromBits(random); + scope.sendingData = false; + + scope.currentFormStep = 0; + + scope.stateData = StateDataService.getData(); + + scope.signupLink = ConfigService.signupLink; + + scope.allowUserResend = false; + scope.censusQuery = "not-sent"; + + scope.code = null; + if (attrs.code && attrs.code.length > 0) { + scope.code = attrs.code; + } + scope.email = null; + if (attrs.email && attrs.email.length > 0) { + scope.email = attrs.email; + } + + scope.isAdmin = false; + if (autheventid === adminId) { + scope.isAdmin = true; + } + + function isValidTel(inputName) { + if (!document.getElementById(inputName)) { + return false; + } + var telInput = angular.element(document.getElementById(inputName)); + return telInput.intlTelInput("isValidNumber"); + } + + function isValidEmail(email) { + var pattern = Patterns.get('email'); + return null !== email.match(pattern); + } + + /** + * Send auth codes now to the voter + */ + scope.resendAuthCode = function(field) { + // if invalid method or already sending data, do not proceed + if ( + scope.sendingData || + !_.contains(["email", "email-otp", "sms", "sms-otp"], scope.method) + ) { + return; + } + + // if telIndex or emailIndex not set when needed, do not proceed + if ( + ( + _.contains(["sms", "sms-otp"], scope.method) && + scope.telIndex === -1 && + !scope.hide_default_login_lookup_field + ) || ( + _.contains(["email", "email-otp"], scope.method) && + scope.emailIndex === -1 && + !scope.hide_default_login_lookup_field + ) + ) { + return; + } + + // obtain the data to be sent to the authapi to request + // new auth codes by filtering and validating login fields + // with steps == undefined or included in step 0 + var stop = false; + var data = _.object( + _.filter( + scope.login_fields, + function (element, index) { + element.index = index; + return ( + element.steps === undefined || + element.steps.indexOf(0) !== -1 + ); + } + ).map( + function (element) { + if ( + ( + _.contains(["sms", "sms-otp"], scope.method) && + element.index === scope.telIndex && + !isValidTel("input" + scope.telIndex) + ) || ( + _.contains(["email", "email-otp"], scope.method) && + element.index === scope.emailIndex && + !isValidEmail(element.value) + ) + ) { + stop = true; + } + return [element.name, element.value]; + } + ) + ); + + // if any issue found, do not proceed + if (stop) { + return; + } + + + // reset code field, as we are going to send a new one + if (!!field) { + field.value = ""; + } + + scope.sendingData = true; + Authmethod.resendAuthCode(data, autheventid) + .then( + function(response) { + // disabling login that are from previous step + _.each( + scope.login_fields, + function (element) { + if ( + element.steps === undefined || + element.steps.indexOf(0) !== -1 + ) { + element.disabled = true; + } + } + ); + scope.currentFormStep = 1; + scope.error = null; + $timeout(scope.sendingDataTimeout, 3000); + }, + function onError(response) { + $timeout(scope.sendingDataTimeout, 3000); + scope.error = $i18next('avRegistration.errorSendingAuthCode'); + } + ); + }; + + scope.sendingDataTimeout = function () { + scope.sendingData = false; + }; + + scope.checkCensus = function(valid) { + if (!valid) { + return; + } + + if (scope.sendingData) { + return; + } + scope.censusQuery = "querying"; + + var data = { + 'captcha_code': Authmethod.captcha_code, + }; + _.each(scope.login_fields, function (field) { + data[field.name] = field.value; + }); + + scope.sendingData = true; + Authmethod.censusQuery(data, autheventid) + .then( + function onSuccess(response) { + scope.sendingData = false; + scope.censusQueryData = response.data; + scope.censusQuery = "success"; + }, + function onError(response) { + scope.sendingData = false; + scope.censusQuery = "fail"; + } + ); + }; + + scope.loginUser = function(valid) { + if (!valid) { + return; + } + if (scope.sendingData) { + return; + } + + // loginUser + if (_.contains(['sms-otp', 'email-otp'], scope.method) && scope.currentFormStep === 0) { + scope.resendAuthCode(); + return; + } + var data = { + 'captcha_code': Authmethod.captcha_code, + }; + _.each(scope.login_fields, function (field) { + if (field.name === 'email') { + scope.email = field.value; + } else if ('code' === field.name) { + field.value = field.value.trim().replace(/ |\n|\t|-|_/g,'').toUpperCase(); } - autheventCookie && autheventCookie === adminId && autheventid === adminId && authCookie && ($window.location.href = "/admin/elections"), - scope.sendingData = !1, scope.currentFormStep = 0, scope.stateData = StateDataService.getData(), - scope.signupLink = ConfigService.signupLink, scope.allowUserResend = !1, scope.censusQuery = "not-sent", - scope.code = null, attrs.code && 0 < attrs.code.length && (scope.code = attrs.code), - scope.email = null, attrs.email && 0 < attrs.email.length && (scope.email = attrs.email), - scope.isAdmin = !1, autheventid === adminId && (scope.isAdmin = !0), scope.resendAuthCode = function(field) { - var stop, data; - !scope.sendingData && _.contains([ "email", "email-otp", "sms", "sms-otp" ], scope.method) && (_.contains([ "sms", "sms-otp" ], scope.method) && -1 === scope.telIndex && !scope.hide_default_login_lookup_field || _.contains([ "email", "email-otp" ], scope.method) && -1 === scope.emailIndex && !scope.hide_default_login_lookup_field || (stop = !1, - data = _.object(_.filter(scope.login_fields, function(element, index) { - return element.index = index, void 0 === element.steps || -1 !== element.steps.indexOf(0); - }).map(function(element) { - var email, pattern; - return (_.contains([ "sms", "sms-otp" ], scope.method) && element.index === scope.telIndex && (pattern = "input" + scope.telIndex, - !document.getElementById(pattern) || !angular.element(document.getElementById(pattern)).intlTelInput("isValidNumber")) || _.contains([ "email", "email-otp" ], scope.method) && element.index === scope.emailIndex && (email = element.value, - pattern = Patterns.get("email"), null === email.match(pattern))) && (stop = !0), - [ element.name, element.value ]; - })), stop || (field && (field.value = ""), scope.sendingData = !0, Authmethod.resendAuthCode(data, autheventid).then(function(response) { - _.each(scope.login_fields, function(element) { - void 0 !== element.steps && -1 === element.steps.indexOf(0) || (element.disabled = !0); - }), scope.currentFormStep = 1, scope.error = null, $timeout(scope.sendingDataTimeout, 3e3); - }, function(response) { - $timeout(scope.sendingDataTimeout, 3e3), scope.error = $i18next("avRegistration.errorSendingAuthCode"); - })))); - }, scope.sendingDataTimeout = function() { - scope.sendingData = !1; - }, scope.checkCensus = function(valid) { - var data; - valid && (scope.sendingData || (scope.censusQuery = "querying", data = { - captcha_code: Authmethod.captcha_code - }, _.each(scope.login_fields, function(field) { - data[field.name] = field.value; - }), scope.sendingData = !0, Authmethod.censusQuery(data, autheventid).then(function(response) { - scope.sendingData = !1, scope.censusQueryData = response.data, scope.censusQuery = "success"; - }, function(response) { - scope.sendingData = !1, scope.censusQuery = "fail"; - }))); - }, scope.loginUser = function(valid) { - var data; - valid && (scope.sendingData || (_.contains([ "sms-otp", "email-otp" ], scope.method) && 0 === scope.currentFormStep ? scope.resendAuthCode() : (data = { - captcha_code: Authmethod.captcha_code - }, _.each(scope.login_fields, function(field) { - "email" === field.name ? scope.email = field.value : "code" === field.name && (field.value = field.value.trim().replace(/ |\n|\t|-|_/g, "").toUpperCase()), - data[field.name] = field.value; - }), "smart-link" === scope.method && (data["auth-token"] = $location.search()["auth-token"]), - scope.sendingData = !0, scope.error = null, Authmethod.login(data, autheventid).then(function(tokens) { - var postfix, options; - "ok" === tokens.data.status ? (postfix = "_authevent_" + autheventid, options = {}, - ConfigService.cookies && ConfigService.cookies.expires && (options.expires = new Date(), - options.expires.setMinutes(options.expires.getMinutes() + ConfigService.cookies.expires)), - $cookies.put("authevent_" + autheventid, autheventid, options), $cookies.put("userid" + postfix, tokens.data.username, options), - $cookies.put("user" + postfix, scope.email || tokens.data.username || tokens.data.email, options), - $cookies.put("auth" + postfix, tokens.data["auth-token"], options), $cookies.put("isAdmin" + postfix, scope.isAdmin, options), - Authmethod.setAuth($cookies.get("auth" + postfix), scope.isAdmin, autheventid), - scope.isAdmin ? Authmethod.getUserInfo().then(function(response) { - $cookies.put("user" + postfix, response.data.email || scope.email || response.data.username, options), - $window.location.href = "/admin/elections"; - }, function(response) { - $window.location.href = "/admin/elections"; - }) : angular.isDefined(tokens.data["redirect-to-url"]) ? $window.location.href = tokens.data["redirect-to-url"] : angular.isDefined(tokens.data["vote-permission-token"]) ? ($window.sessionStorage.setItem("vote_permission_tokens", JSON.stringify([ { + data[field.name] = field.value; + }); + + // Get the smart link authentication token and set it in the data if + // this is an auth event with smart-link auth method + if (scope.method === 'smart-link') + { + data['auth-token'] = $location.search()['auth-token']; + } + + scope.sendingData = true; + scope.error = null; + Authmethod + .login(data, autheventid) + .then( + function onSuccess(response) { + if (response.data.status === "ok") { + var postfix = "_authevent_" + autheventid; + var options = {}; + if (ConfigService.cookies && ConfigService.cookies.expires) { + options.expires = new Date(); + options.expires.setMinutes(options.expires.getMinutes() + ConfigService.cookies.expires); + } + $cookies.put("authevent_" + autheventid, autheventid, options); + $cookies.put("userid" + postfix, response.data.username, options); + $cookies.put("user" + postfix, scope.email || response.data.username || response.data.email, options); + $cookies.put("auth" + postfix, response.data['auth-token'], options); + $cookies.put("isAdmin" + postfix, scope.isAdmin, options); + Authmethod.setAuth($cookies.get("auth" + postfix), scope.isAdmin, autheventid); + if (scope.isAdmin) + { + Authmethod.getUserInfo() + .then( + function onSuccess(response) { + $cookies.put("user" + postfix, response.data.email || scope.email || response.data.username, options); + $window.location.href = '/admin/elections'; + }, + function onError(response) { + $window.location.href = '/admin/elections'; + } + ); + } + else if (angular.isDefined(response.data['redirect-to-url'])) + { + $window.location.href = response.data['redirect-to-url']; + } + // if it's an election with no children elections + else if (angular.isDefined(response.data['vote-permission-token'])) + { + $window.sessionStorage.setItem( + "vote_permission_tokens", + JSON.stringify([{ electionId: autheventid, - token: tokens.data["vote-permission-token"] - } ])), $window.location.href = "/booth/" + autheventid + "/vote") : angular.isDefined(tokens.data["vote-children-info"]) ? (tokens = _.chain(tokens.data["vote-children-info"]).filter(function(child) { - return (0 === child["num-successful-logins-allowed"] || child["num-successful-logins"] < child["num-successful-logins-allowed"]) && !!child["vote-permission-token"]; - }).map(function(child, index) { + token: response.data['vote-permission-token'] + }]) + ); + $window.location.href = '/booth/' + autheventid + '/vote'; + } + // if it's an election with children elections then show access to them + else if (angular.isDefined(response.data['vote-children-info'])) + { + // assumes the authapi response has the same children + var tokens = _ + .chain(response.data['vote-children-info']) + .filter(function (child) { + return ( + child['num-successful-logins-allowed'] === 0 || + child['num-successful-logins'] < child['num-successful-logins-allowed'] + ) && !!child['vote-permission-token']; + }) + .map(function (child, index) { return { - electionId: child["auth-event-id"], - token: child["vote-permission-token"], - skipped: !1, - voted: !1, - isFirst: 0 === index + electionId: child['auth-event-id'], + token: child['vote-permission-token'], + skipped: false, + voted: false, + isFirst: index === 0 }; - }).value(), $window.sessionStorage.setItem("vote_permission_tokens", JSON.stringify(tokens)), - 0 < tokens.length ? $window.location.href = "/booth/" + tokens[0].electionId + "/vote" : scope.error = $i18next("avRegistration.invalidCredentials", { - support: ConfigService.contact.email - })) : scope.error = $i18next("avRegistration.invalidCredentials", { - support: ConfigService.contact.email - })) : (scope.sendingData = !1, scope.status = "Not found", scope.error = $i18next("avRegistration.invalidCredentials", { - support: ConfigService.contact.email - })); - }, function(response) { - scope.sendingData = !1, scope.status = "Registration error: " + response.data.message, - scope.error = $i18next("avRegistration.invalidCredentials", { - support: ConfigService.contact.email - }); - })))); - }, scope.apply = function(authevent) { - var electionsMatch, adminMatch; - scope.method = authevent.auth_method, scope.name = authevent.name, scope.registrationAllowed = "open" === authevent.census && (autheventid !== adminId || ConfigService.allowAdminRegistration), - scope.isCensusQuery ? scope.login_fields = Authmethod.getCensusQueryFields(authevent) : scope.login_fields = Authmethod.getLoginFields(authevent), - scope.hide_default_login_lookup_field = authevent.hide_default_login_lookup_field, - scope.telIndex = -1, scope.emailIndex = -1, scope.telField = null, scope.allowUserResend = (fields = !1, - electionsMatch = $location.path(), adminMatch = electionsMatch.match(/^\/admin\//), - electionsMatch = electionsMatch.match(/^\/(elections|election)\/([0-9]+)\//), _.isArray(adminMatch) ? fields = !0 : _.isArray(electionsMatch) && 3 === electionsMatch.length && (fields = _.isObject(authevent.auth_method_config) && _.isObject(authevent.auth_method_config.config) && !0 === authevent.auth_method_config.config.allow_user_resend), - fields); - var fields = _.map(scope.login_fields, function(el, index) { - return scope.stateData[el.name] ? (el.value = scope.stateData[el.name], el.disabled = !0) : (el.value = null, - el.disabled = !1), "email" === el.type ? (null !== scope.email && (el.value = scope.email, - el.disabled = !0, "email-otp" === scope.method && (scope.currentFormStep = 1)), - scope.emailIndex = index) : "code" === el.type && null !== scope.code ? (el.value = scope.code.trim().replace(/ |\n|\t|-|_/g, "").toUpperCase(), - el.disabled = !0) : "tlf" === el.type && "sms" === scope.method ? (null !== scope.email && -1 === scope.email.indexOf("@") && (el.value = scope.email, - el.disabled = !0), scope.telIndex = index + 1, scope.telField = el) : "tlf" === el.type && "sms-otp" === scope.method && (null !== scope.email && -1 === scope.email.indexOf("@") && (el.value = scope.email, - el.disabled = !0, scope.currentFormStep = 1), scope.telIndex = index + 1, scope.telField = el), - el; - }); - _.filter(fields, function(el) { - return null !== el.value; - }).length === scope.login_fields.length && "openid-connect" !== scope.method && scope.loginUser(!0); - }, scope.view = function(id) { - Authmethod.viewEvent(id).then(function(response) { - "ok" === response.data.status ? scope.apply(response.data.events) : (scope.status = "Not found", - document.querySelector(".input-error").style.display = "block"); - }, function(response) { - scope.status = "Scan error: " + response.data.message, document.querySelector(".input-error").style.display = "block"; - }); - }, scope.view(autheventid), scope.goSignup = function() { - $state.go("registration.register", { - id: autheventid - }); - }, scope.forgotPassword = function() { - console.log("forgotPassword"); - }, scope.openidConnectAuth = function(provider) { - var randomState = randomStr(), authURI = randomStr(); - $cookies["openid-connect-csrf"] = angular.toJson({ - randomState: randomState, - randomNonce: authURI, - created: Date.now(), - eventId: scope.eventId, - providerId: provider.id - }), provider ? (authURI = provider.authorization_endpoint + "?response_type=id_token&client_id=" + encodeURIComponent(provider.client_id) + "&scope=" + encodeURIComponent("openid") + "&redirect_uri=" + encodeURIComponent($window.location.origin + "/election/login-openid-connect-redirect") + "&state=" + randomState + "&nonce=" + authURI, - $window.location.href = authURI) : scope.error = $i18next("avRegistration.openidError"); - }; - }, - templateUrl: "avRegistration/login-directive/login-directive.html" - }; -} ]), angular.module("avRegistration").directive("avOpenidConnect", [ "$cookies", "$window", "$location", "ConfigService", "Authmethod", function($cookies, $window, $location, ConfigService, Authmethod) { + }) + .value(); + $window.sessionStorage.setItem( + "vote_permission_tokens", + JSON.stringify(tokens) + ); + + if (tokens.length > 0) { + $window.location.href = '/booth/' + tokens[0].electionId + '/vote'; + } else { + scope.error = $i18next( + 'avRegistration.invalidCredentials', + {support: ConfigService.contact.email} + ); + } + } else { + scope.error = $i18next( + 'avRegistration.invalidCredentials', + {support: ConfigService.contact.email} + ); + } + } else { + scope.sendingData = false; + scope.status = 'Not found'; + scope.error = $i18next( + 'avRegistration.invalidCredentials', + {support: ConfigService.contact.email} + ); + } + }, + function onError(response) { + scope.sendingData = false; + scope.status = 'Registration error: ' + response.data.message; + scope.error = $i18next( + 'avRegistration.invalidCredentials', + {support: ConfigService.contact.email} + ); + } + ); + }; + + scope.apply = function(authevent) { + scope.method = authevent['auth_method']; + scope.name = authevent['name']; + scope.registrationAllowed = ( + (authevent['census'] === 'open') && + (autheventid !== adminId || ConfigService.allowAdminRegistration) + ); + if (!scope.isCensusQuery && !scope.withCode) { + scope.login_fields = Authmethod.getLoginFields(authevent); + } else if (scope.withCode) { + scope.login_fields = Authmethod.getLoginWithCode(authevent); + } else { // scope.isCensusQuery is true + scope.login_fields = Authmethod.getCensusQueryFields(authevent); + } + scope.hide_default_login_lookup_field = authevent.hide_default_login_lookup_field; + scope.telIndex = -1; + scope.emailIndex = -1; + scope.telField = null; + scope.allowUserResend = (function () { + if (scope.withCode) { + return false; + } + var ret = false; + var href = $location.path(); + var adminMatch = href.match(/^\/admin\//); + var electionsMatch = href.match(/^\/(elections|election)\/([0-9]+)\//); + + if (_.isArray(adminMatch)) { + ret = true; + } else if (_.isArray(electionsMatch) && 3 === electionsMatch.length) { + ret = (_.isObject(authevent.auth_method_config) && + _.isObject(authevent.auth_method_config.config) && + true === authevent.auth_method_config.config.allow_user_resend); + } + return ret; + })(); + + var fields = _.map( + scope.login_fields, + function (el, index) { + if (!!scope.stateData[el.name]) { + el.value = scope.stateData[el.name]; + el.disabled = true; + } else { + el.value = null; + el.disabled = false; + } + if (el.type === "email") { + if (scope.email !== null) { + el.value = scope.email; + el.disabled = true; + if (scope.method === "email-otp") { + scope.currentFormStep = 1; + } + } + scope.emailIndex = index; + } else if (el.type === "code" && scope.code !== null) { + el.value = scope.code.trim().replace(/ |\n|\t|-|_/g,'').toUpperCase(); + el.disabled = true; + } else if (el.type === "tlf" && scope.method === "sms") { + if (scope.email !== null && scope.email.indexOf('@') === -1) { + el.value = scope.email; + el.disabled = true; + } + scope.telIndex = index+1; + scope.telField = el; + } else if (el.type === "tlf" && scope.method === "sms-otp") { + if (scope.email !== null && scope.email.indexOf('@') === -1) { + el.value = scope.email; + el.disabled = true; + scope.currentFormStep = 1; + } + scope.telIndex = index+1; + scope.telField = el; + } else if (el.name === '__username' && scope.withCode) { + el.value = scope.username; + el.disabled = true; + } + return el; + }); + var filled_fields = _.filter(fields, + function (el) { return el.value !== null; }); + + // if not all the fields all filled at this point, then we stop here + if (filled_fields.length !== scope.login_fields.length) { + return; + } + + // if all fields all filled in and it's not OpenID Connect do + // auto-login + if (scope.method !== 'openid-connect') + { + scope.loginUser(true); + } + + }; + + scope.view = function(id) { + Authmethod.viewEvent(id) + .then( + function onSuccess(response) { + if (response.data.status === "ok") { + scope.apply(response.data.events); + } else { + scope.status = 'Not found'; + document.querySelector(".input-error").style.display = "block"; + } + }, + function onError(response) { + scope.status = 'Scan error: ' + response.data.message; + document.querySelector(".input-error").style.display = "block"; + } + ); + }; + scope.view(autheventid); + + scope.goSignup = function() { + $state.go('registration.register', {id: autheventid}); + }; + + scope.forgotPassword = function() { + console.log('forgotPassword'); + }; + + // generate a cryptogrpahically secure random string + function randomStr() + { + /* jshint ignore:start */ + var random = sjcl.random.randomWords(/* bitlength */ 2048 / 32, 0); + return sjcl.codec.hex.fromBits(random); + /* jshint ignore:end */ + } + + // OpenIDConnect sets a cookie that is used to create a CSRF token + // similar to what is mentioned here: + // https://developers.google.com/identity/protocols/OpenIDConnect#createxsrftoken + scope.openidConnectAuth = function(provider) + { + var randomState = randomStr(); + var randomNonce = randomStr(); + $cookies['openid-connect-csrf'] = angular.toJson({ + randomState: randomState, + randomNonce: randomNonce, + created: Date.now(), + eventId: scope.eventId, + providerId: provider.id + }); + + // find provider + if (!provider) + { + scope.error = $i18next('avRegistration.openidError'); + return; + } + + // Craft the OpenID Connect auth URI + var authURI = (provider.authorization_endpoint + + "?response_type=id_token" + + "&client_id=" + encodeURIComponent(provider.client_id) + + "&scope=" + encodeURIComponent("openid") + + "&redirect_uri=" + encodeURIComponent( + $window.location.origin + + "/election/login-openid-connect-redirect" + ) + + "&state=" + randomState + + "&nonce=" + randomNonce + ); + + // Redirect to the Auth URI + $window.location.href = authURI; + }; + } return { - restrict: "AE", - scope: !0, - link: function(scope, element, attrs) { - var maxOAuthLoginTimeout = 3e5; - function simpleRedirectToLogin() { - scope.csrf ? $window.location.href = "/election/" + scope.csrf.eventId + "/public/login" : $window.location.href = "/"; + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avRegistration/login-directive/login-directive.html' + }; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive( + 'avOpenidConnect', + function( + $cookies, + $window, + $location, + ConfigService, + Authmethod + ) { + // we use it as something similar to a controller here + function link(scope, element, attrs) + { + // Maximum Oauth Login Timeout is 5 minutes + var maxOAuthLoginTimeout = 1000 * 60 * 5; + + scope.csrf = null; + + // simply redirect to login + function simpleRedirectToLogin() + { + if (scope.csrf) + { + $window.location.href = "/election/" + scope.csrf.eventId + "/public/login"; + } else { + $window.location.href = "/"; + } + } + + // Returns the logout url if any from the appropiate openidprovider + // TODO: logout asumes that you are using the first provider, so it + // basically supports only one provider + function getLogoutUri() + { + if (ConfigService.openIDConnectProviders.length === 0 || !ConfigService.openIDConnectProviders[0].logout_uri) + { + return false; + } + + var eventId = null; + if (scope.csrf) + { + eventId = scope.csrf.eventId; + } + + var uri = ConfigService.openIDConnectProviders[0].logout_uri; + uri = uri.replace("__EVENT_ID__", "" + eventId); + + var postfix = "_authevent_" + eventId; + if (!!$cookies.get("id_token_" + postfix)) + { + uri = uri.replace("__ID_TOKEN__", $cookies.get("id_token_" + postfix)); + + // if __ID_TOKEN__ is there but we cannot replace it, we need to + // directly redirect to the login, otherwise the URI might show an + // error 500 + } else if (uri.indexOf("__ID_TOKEN__") > -1) + { + uri = "/election/" + eventId + "/public/login"; + } + + return uri; + } + + scope.redirectingToUri = false; + + // Redirects to the login page of the respective event_id if any + function redirectToLogin() + { + if (scope.redirectingToUri) + { + return; + } + + scope.redirectingToUri = true; + + var eventId = null; + if (scope.csrf) + { + eventId = scope.csrf.eventId; + } else { + $window.location.href = "/"; + return; + } + + Authmethod.viewEvent(eventId) + .then( + function onSuccess(response) + { + if (response.data.status !== "ok" || !response.data.events || response.data.events.auth_method !== 'openid-connect' || !getLogoutUri()) + { + simpleRedirectToLogin(); + return; + } + + var postfix = "_authevent_" + eventId; + var uri = getLogoutUri(); + $cookies.remove("id_token_" + postfix); + $window.location.href = uri; + }, + function onError(response) + { + simpleRedirectToLogin(); + } + ); + } + + // Get the decoded value of a uri parameter from any uri. The uri does not + // need to have any domain, it can start with the character "?" + function getURIParameter(paramName, uri) + { + var paramName2 = paramName.replace(/[\[\]]/g, '\\$&'); + var rx = new RegExp('[?&]' + paramName2 + '(=([^&#]*)|&|#|$)'); + var params = rx.exec(uri); + + if (!params) + { + return null; + } + + if (!params[2]) + { + return ''; } - function getLogoutUri() { - if (0 === ConfigService.openIDConnectProviders.length || !ConfigService.openIDConnectProviders[0].logout_uri) return !1; - var eventId = null; - scope.csrf && (eventId = scope.csrf.eventId); - var uri = (uri = ConfigService.openIDConnectProviders[0].logout_uri).replace("__EVENT_ID__", "" + eventId), postfix = "_authevent_" + eventId; - return $cookies.get("id_token_" + postfix) ? uri = uri.replace("__ID_TOKEN__", $cookies.get("id_token_" + postfix)) : -1 < uri.indexOf("__ID_TOKEN__") && (uri = "/election/" + eventId + "/public/login"), - uri; + return decodeURIComponent(params[2].replace(/\+/g, ' ')); + } + + // validates the CSRF token + function validateCsrfToken() + { + if (!$cookies.get('openid-connect-csrf')) + { + redirectToLogin(); + return null; } - function redirectToLogin() { - var eventId; - scope.redirectingToUri || (scope.redirectingToUri = !0, eventId = null, scope.csrf ? (eventId = scope.csrf.eventId, - Authmethod.viewEvent(eventId).then(function(uri) { - var postfix; - "ok" === uri.data.status && uri.data.events && "openid-connect" === uri.data.events.auth_method && getLogoutUri() ? (postfix = "_authevent_" + eventId, - uri = getLogoutUri(), $cookies.remove("id_token_" + postfix), $window.location.href = uri) : simpleRedirectToLogin(); - }, function(response) { - simpleRedirectToLogin(); - })) : $window.location.href = "/"); + + // validate csrf token format and data + var csrf = scope.csrf = angular.fromJson($cookies.get('openid-connect-csrf')); + var uri = "?" + $window.location.hash.substr(1); + + // NOTE: if you need to debug this callback, obtain the callback + // URL, get the callback received in the server (to obtain the + // nonce) that was received by the client and change the data here + // accordingly and set here the debug break point, then execute + // a line like the following in the comment. + // + // The only data that needs to be changed is the randomNonnce and + // the eventId. + // + // csrf = scope.csrf = { + // randomNonce: 'something', + // randomState: getURIParameter("state", uri), + // created: Date.now(), + // eventId: 11111 + // }; + + $cookies.remove('openid-connect-csrf'); + var isCsrfValid = (!!csrf && + angular.isObject(csrf) && + angular.isString(csrf.randomState) && + angular.isString(csrf.randomNonce) && + angular.isNumber(csrf.created) && + getURIParameter("state", uri) === csrf.randomState && + csrf.created - Date.now() < maxOAuthLoginTimeout + ); + + if (!isCsrfValid) + { + redirectToLogin(); + return null; } - function getURIParameter(paramName2, params) { - paramName2 = paramName2.replace(/[\[\]]/g, "\\$&"), params = new RegExp("[?&]" + paramName2 + "(=([^&#]*)|&|#|$)").exec(params); - return params ? params[2] ? decodeURIComponent(params[2].replace(/\+/g, " ")) : "" : null; + return true; + } + + // Process an OpenId Connect callback coming from the provider, try to + // validate the callback data and get the authentication token from our + // server and redirect to vote + function processOpenIdAuthCallback() + { + // validate csrf token from uri and from state in the hash + validateCsrfToken(); + + var uri = "?" + $window.location.hash.substr(1); + + var data = { + id_token: getURIParameter("id_token", uri), + provider: scope.csrf.providerId, + nonce: scope.csrf.randomNonce + }; + + var options = {}; + if (ConfigService.cookies && ConfigService.cookies.expires) { + options.expires = new Date(); + options.expires.setMinutes(options.expires.getMinutes() + ConfigService.cookies.expires); } - scope.csrf = null, scope.redirectingToUri = !1, function() { - !function() { - if ($cookies.get("openid-connect-csrf")) { - var csrf = scope.csrf = angular.fromJson($cookies.get("openid-connect-csrf")), uri = "?" + $window.location.hash.substr(1); - if ($cookies.remove("openid-connect-csrf"), !!csrf && angular.isObject(csrf) && angular.isString(csrf.randomState) && angular.isString(csrf.randomNonce) && angular.isNumber(csrf.created) && getURIParameter("state", uri) === csrf.randomState && csrf.created - Date.now() < maxOAuthLoginTimeout) return; + + var postfix = "_authevent_" + scope.csrf.eventId; + $cookies.put("id_token_" + postfix, data.id_token, options); + + // Send the authentication request to our server + Authmethod.login(data, scope.csrf.eventId) + .then( + function onSuccess(response) + { + if (response.data.status === "ok") + { + scope.khmac = response.data.khmac; + var postfix = "_authevent_" + scope.csrf.eventId; + $cookies.put("authevent_" + scope.csrf.eventId, scope.csrf.eventId, options); + $cookies.put("userid" + postfix, response.data.username, options); + $cookies.put("user" + postfix, response.data.username, options); + $cookies.put("auth" + postfix, response.data['auth-token'], options); + $cookies.put("isAdmin" + postfix, false, options); + Authmethod.setAuth($cookies.get("auth" + postfix), scope.isAdmin, scope.csrf.eventId); + + if (angular.isDefined(response.data['redirect-to-url'])) + { + $window.location.href = response.data['redirect-to-url']; + } + else + { + // redirecting to vote link + Authmethod.getPerm("vote", "AuthEvent", scope.csrf.eventId) + .then(function onSuccess(response2) + { + var khmac = response2.data['permission-token']; + var path = khmac.split(";")[1]; + var hash = path.split("/")[0]; + var msg = path.split("/")[1]; + $window.location.href = '/booth/' + scope.csrf.eventId + '/vote/' + hash + '/' + msg; + }); + } + } else + { + // TODO: show error redirectToLogin(); - } else redirectToLogin(); - }(); - var data = { - id_token: getURIParameter("id_token", "?" + $window.location.hash.substr(1)), - provider: scope.csrf.providerId, - nonce: scope.csrf.randomNonce - }, options = {}; - ConfigService.cookies && ConfigService.cookies.expires && (options.expires = new Date(), - options.expires.setMinutes(options.expires.getMinutes() + ConfigService.cookies.expires)); - var postfix = "_authevent_" + scope.csrf.eventId; - $cookies.put("id_token_" + postfix, data.id_token, options), Authmethod.login(data, scope.csrf.eventId).then(function(response) { - var postfix; - "ok" === response.data.status ? (scope.khmac = response.data.khmac, postfix = "_authevent_" + scope.csrf.eventId, - $cookies.put("authevent_" + scope.csrf.eventId, scope.csrf.eventId, options), $cookies.put("userid" + postfix, response.data.username, options), - $cookies.put("user" + postfix, response.data.username, options), $cookies.put("auth" + postfix, response.data["auth-token"], options), - $cookies.put("isAdmin" + postfix, !1, options), Authmethod.setAuth($cookies.get("auth" + postfix), scope.isAdmin, scope.csrf.eventId), - angular.isDefined(response.data["redirect-to-url"]) ? $window.location.href = response.data["redirect-to-url"] : Authmethod.getPerm("vote", "AuthEvent", scope.csrf.eventId).then(function(hash) { - var msg = hash.data["permission-token"].split(";")[1], hash = msg.split("/")[0], msg = msg.split("/")[1]; - $window.location.href = "/booth/" + scope.csrf.eventId + "/vote/" + hash + "/" + msg; - })) : redirectToLogin(); - }, function(response) { + return; + } + }, + function onError(response) + { + // TODO: show error redirectToLogin(); - }); - }(); - }, - templateUrl: "avRegistration/openid-connect-directive/openid-connect-directive.html" - }; -} ]), angular.module("avRegistration").controller("LogoutController", [ "$scope", "$stateParams", "$filter", "ConfigService", "$i18next", "$state", "$cookies", "Authmethod", function($scope, $stateParams, $filter, ConfigService, $i18next, $state, $cookies, postfix) { - ConfigService.freeAuthId; - var authevent = postfix.getAuthevent(), postfix = "_authevent_" + authevent; - $cookies.put("user" + postfix, ""), $cookies.put("auth" + postfix, ""), $cookies.put("authevent_" + authevent, ""), - $cookies.put("userid" + postfix, ""), $cookies.put("isAdmin" + postfix, !1), authevent !== ConfigService.freeAuthId + "" && authevent ? $state.go("registration.login", { - id: $cookies.get("authevent_" + authevent) - }) : $state.go("admin.login"); -} ]), angular.module("avRegistration").controller("RegisterController", [ "$scope", "$stateParams", "$filter", "ConfigService", "$i18next", function($scope, $stateParams, $filter, ConfigService, $i18next) { - $scope.event_id = $stateParams.id, $scope.email = $stateParams.email; -} ]), angular.module("avRegistration").directive("avRegister", [ "Authmethod", "StateDataService", "$parse", "$state", "ConfigService", "$cookies", "$i18next", "$sce", function(Authmethod, StateDataService, $parse, $state, ConfigService, $cookies, $i18next, $sce) { - return { - restrict: "AE", - scope: !0, - link: function(scope, element, attrs) { - var autheventid = attrs.eventId; - scope.dnieurl = ConfigService.dnieUrl + autheventid + "/", scope.register = {}, - scope.sendingData = !1, scope.admin = !1, scope.error = null, scope.email = null, - attrs.email && 0 < attrs.email.length && (scope.email = attrs.email), "admin" in attrs && (scope.admin = !0), - scope.getLoginDetails = function(eventId) { - return scope.admin ? { - path: "admin.login_email", - data: { - email: scope.email + return; + } + ); + } + + processOpenIdAuthCallback(); + } + return { + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avRegistration/openid-connect-directive/openid-connect-directive.html' + }; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration').controller('LogoutController', + function($scope, $stateParams, $filter, ConfigService, $i18next, $state, $cookies, Authmethod) { + var adminId = ConfigService.freeAuthId; + var authevent = Authmethod.getAuthevent(); + var postfix = "_authevent_" + authevent; + $cookies.put("user" + postfix, ''); + $cookies.put("auth" + postfix, ''); + $cookies.put("authevent_" + authevent, ''); + $cookies.put("userid" + postfix, ''); + $cookies.put("isAdmin" + postfix, false); + if (authevent === ConfigService.freeAuthId + '' || !authevent) { + $state.go("admin.login"); + } else { + $state.go("registration.login", {id: $cookies.get("authevent_" + authevent)}); + } + } +); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration').controller('RegisterController', + function($scope, $stateParams, $filter, ConfigService, $i18next) { + $scope.event_id = $stateParams.id; + $scope.email = $stateParams.email; + } +); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avRegister', function(Authmethod, StateDataService, $parse, $state, ConfigService, $cookies, $i18next, $sce) { + // we use it as something similar to a controller here + function link(scope, element, attrs) { + var autheventid = attrs.eventId; + scope.dnieurl = ConfigService.dnieUrl + autheventid + '/'; + scope.register = {}; + scope.sendingData = false; + scope.admin = false; + scope.error = null; + + scope.email = null; + if (attrs.email && attrs.email.length > 0) { + scope.email = attrs.email; + } + + if ("admin" in attrs) { + scope.admin = true; + } + + scope.getLoginDetails = function (eventId) { + if (!scope.admin) { + return { + path: 'election.public.show.login_email', + data: {id: eventId, email: scope.email} + }; + } else { + return {path: 'admin.login_email', data:{email: scope.email}}; + } + }; + + scope.signUp = function(valid) { + if (!valid) { + return; + } + scope.sendingData = true; + scope.error = null; + var data = { + 'captcha_code': Authmethod.captcha_code, + }; + _.each(scope.register_fields, function (field) { + data[field.name] = field.value; + if (field.name === 'email' && _.contains(['email', 'email-otp'], scope.method)) + { + scope.email = field.value; + } + else if (field.name === 'tlf' && + _.contains(['sms', 'sms-otp'], scope.method)) + { + scope.email = field.value; + } + }); + var details; + Authmethod.signup(data, autheventid) + .then( + function onSuccess(response) { + details = scope.getLoginDetails(autheventid); + if (response.data.status === "ok") { + scope.user = response.data.user; + StateDataService.go(details.path, details.data, data); + scope.error = response.data.msg || $sce.trustAsHtml($i18next('avRegistration.invalidRegisterData', { + url: $state.href(details.path, details.data) + })); + } else { + scope.sendingData = false; + scope.status = 'Not found'; + scope.error = response.data.msg || $sce.trustAsHtml($i18next('avRegistration.invalidRegisterData', { + url: $state.href(details.path, details.data) + })); } - } : { - path: "election.public.show.login_email", - data: { - id: eventId, - email: scope.email + }, + function onError(response) { + details = scope.getLoginDetails(autheventid); + scope.sendingData = false; + scope.status = 'Registration error: ' + response.data.message; + + if (!!response.data.error_codename && response.data.error_codename === 'invalid-dni') { + scope.error = $sce.trustAsHtml($i18next('avRegistration.invalidRegisterDNI')); + } else { + scope.error = response.data.msg || $sce.trustAsHtml($i18next('avRegistration.invalidRegisterData', { + url: $state.href(details.path, details.data) + })); + if (response.data.msg === 'Invalid captcha') { + Authmethod.newCaptcha(); + } } - }; - }, scope.signUp = function(valid) { - var data, details; - valid && (scope.sendingData = !0, scope.error = null, data = { - captcha_code: Authmethod.captcha_code - }, _.each(scope.register_fields, function(field) { - data[field.name] = field.value, ("email" === field.name && _.contains([ "email", "email-otp" ], scope.method) || "tlf" === field.name && _.contains([ "sms", "sms-otp" ], scope.method)) && (scope.email = field.value); - }), Authmethod.signup(data, autheventid).then(function(response) { - details = scope.getLoginDetails(autheventid), "ok" === response.data.status ? (scope.user = response.data.user, - StateDataService.go(details.path, details.data, data)) : (scope.sendingData = !1, - scope.status = "Not found"), scope.error = response.data.msg || $sce.trustAsHtml($i18next("avRegistration.invalidRegisterData", { - url: $state.href(details.path, details.data) - })); - }, function(response) { - details = scope.getLoginDetails(autheventid), scope.sendingData = !1, scope.status = "Registration error: " + response.data.message, - response.data.error_codename && "invalid-dni" === response.data.error_codename ? scope.error = $sce.trustAsHtml($i18next("avRegistration.invalidRegisterDNI")) : (scope.error = response.data.msg || $sce.trustAsHtml($i18next("avRegistration.invalidRegisterData", { - url: $state.href(details.path, details.data) - })), "Invalid captcha" === response.data.msg && Authmethod.newCaptcha()); - })); - }, scope.goLogin = function(event) { - console.log("goLogin"), event && (event.preventDefault(), event.stopPropagation()), - scope.authevent && (scope.authevent.id === ConfigService.freeAuthId ? $state.go("admin.login") : $state.go("election.public.show.login", { - id: scope.authevent.id - })); - }, scope.apply = function(authevent) { - scope.method = authevent.auth_method, scope.name = authevent.name, "open" === (scope.authevent = authevent).census && "openid-connect" !== scope.method || (authevent.id === ConfigService.freeAuthId ? $state.go("admin.login") : $state.go("election.public.show.login", { - id: authevent.id - })), scope.register_fields = Authmethod.getRegisterFields(authevent); - _.map(scope.register_fields, function(el) { - return el.value = null, el.disabled = !1, "email" === el.type && null !== scope.email && (el.value = scope.email, - el.disabled = !0), el; - }); - }, scope.view = function(id) { - Authmethod.viewEvent(id).then(function(response) { - "ok" === response.data.status ? scope.apply(response.data.events) : (scope.status = "Not found", - document.querySelector(".input-error").style.display = "block"); - }, function(response) { - scope.status = "Scan error: " + response.data.message, document.querySelector(".input-error").style.display = "block"; - }); - }, scope.view(autheventid); - }, - templateUrl: "avRegistration/register-directive/register-directive.html" + } + ); + }; + + scope.goLogin = function(event) { + console.log("goLogin"); + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + if (!scope.authevent) { + return; + } + + if (scope.authevent['id'] === ConfigService.freeAuthId) { + $state.go("admin.login"); + } else { + $state.go("election.public.show.login", {id: scope.authevent['id']}); + } + }; + + scope.apply = function(authevent) { + scope.method = authevent['auth_method']; + scope.name = authevent['name']; + scope.authevent = authevent; + + // if registration is closed, redirect to login + if (authevent['census'] !== 'open' || scope.method === 'openid-connect') { + if (authevent['id'] === ConfigService.freeAuthId) { + $state.go("admin.login"); + } else { + $state.go("election.public.show.login", {id: authevent['id']}); + } + } + scope.register_fields = Authmethod.getRegisterFields(authevent); + var fields = _.map( + scope.register_fields, + function (el) { + el.value = null; + el.disabled = false; + if (el.type === "email" && scope.email !== null) { + el.value = scope.email; + el.disabled = true; + } + return el; + }); + }; + + scope.view = function(id) { + Authmethod.viewEvent(id) + .then( + function onSuccess(response) { + if (response.data.status === "ok") { + scope.apply(response.data.events); + } else { + scope.status = 'Not found'; + document.querySelector(".input-error").style.display = "block"; + } + }, + function onError(response) { + scope.status = 'Scan error: ' + response.data.message; + document.querySelector(".input-error").style.display = "block"; + } + ); + }; + + scope.view(autheventid); + } + + return { + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avRegistration/register-directive/register-directive.html' + }; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .factory('Patterns', function() { + var patterns = {}; + patterns.get = function(name) { + if (name === 'dni') { + return /^\d{7,8}[a-zA-Z]{1}$/i; + } else if (name === 'mail' || name === 'email') { + return /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + } else { + return /.*/; + } + }; + return patterns; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/** + * Shows a field + */ +angular.module('avRegistration') + .directive('avrField', function($state) { + function link(scope, element, attrs) { + console.log("type = " + scope.field.type); + scope.index = attrs.index; + } + + return { + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avRegistration/field-directive/field-directive.html' + }; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrEmailField', function($state, Patterns) { + function link(scope, element, attrs) { + scope.emailRe = Patterns.get('email'); + } + return { + restrict: 'AE', + link: link, + scope: true, + templateUrl: 'avRegistration/fields/email-field-directive/email-field-directive.html' + }; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrDateField', function($state, Patterns) { + function link(scope, element, attrs) { + scope.years = []; + scope.months = []; + scope.field = scope.$parent.field; + scope.date = null; + + function initializeValue() { + var dateValue = null; + if ( + scope.field.value === null || scope.field.value.length === 0 + ) { + dateValue = new Date(); + } else { + var data = scope.field.value.split('-'); + dateValue = new Date(data[0], parseInt(data[1]) - 1, data[2]); + } + scope.date = { + year: dateValue.getFullYear(), + month: dateValue.getMonth() + 1, + day: dateValue.getDate() + }; + } + initializeValue(); + + scope.getYears = function () { + var initY = (new Date()).getFullYear(); + var i = 0; + var years = []; + + for (i=initY; i>=initY-130; i--) { + years.push(i); + } + return years; + }; + + scope.getMonths = function () { + var i = 0; + var months = []; + + for (i=1; i<=12; i++) { + months.push(i); + } + return months; + }; + + scope.getDays = function() { + var days = []; + var i = 0; + var ndays = (new Date(scope.date.year, scope.date.month, 0)).getDate(); + for (i=1; i<=ndays; i++) { + days.push(i); + } + return days; + }; + + function numberPadStart(num, size) { + var str = "000000000" + num; + return str.substr(str.length - size); + } + + scope.onChange = function() { + var monthStr = numberPadStart(scope.date.month, 2); + var dayStr = numberPadStart(scope.date.day, 2); + scope.field.value = scope.date.year + "-" + monthStr + "-" + dayStr; + }; + + // initial value update + scope.onChange(); + } + return { + restrict: 'AE', + link: link, + scope: { + label: '=', + }, + templateUrl: 'avRegistration/fields/date-field-directive/date-field-directive.html' + }; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrPasswordField', function($state) { + return { + restrict: 'AE', + scope: true, + templateUrl: 'avRegistration/fields/password-field-directive/password-field-directive.html' + }; +}); +angular.module('avRegistration') + .directive('avrTextField', function($state) { + function link(scope, element, attrs) { + if (angular.isUndefined(scope.field.regex)) { + scope.re = new RegExp(""); + } else { + scope.re = new RegExp(scope.field.regex); + } + } + return { + restrict: 'AE', + link: link, + scope: true, + templateUrl: 'avRegistration/fields/text-field-directive/text-field-directive.html' }; -} ]), angular.module("avRegistration").factory("Patterns", function() { - var patterns = { - get: function(name) { - return "dni" === name ? /^\d{7,8}[a-zA-Z]{1}$/i : "mail" === name || "email" === name ? /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ : /.*/; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrDniField', function($state) { + function link(scope, element, attrs) { + var dni_re = /^([0-9]{1,8}[A-Z]|[LMXYZ][0-9]{1,7}[A-Z])$/; + + /** + * Normalizes dnis, using uppercase, removing characters not allowed and + * left-side zeros + */ + function normalize_dni(dni) { + if (!dni) { + return ""; + } + + var allowed_chars = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890"; + var dni2 = dni.toUpperCase(); + var dni3 = ""; + for (var i = 0; i < dni2.lenth; i++) { + var char = dni2[i]; + if (allowed_chars.indexOf(char) >= 0) { + dni3 += char; + } + } + var numbers = "1234567890"; + var last_char = ""; + var dni4 = ""; + for (var j = 0; j < dni3.lenth; j++) { + var char2 = dni3[j]; + if ((last_char==="" || '1234567890'.indexOf(last_char) === -1) && char2 === '0') { + } + dni4 += char2; + last_char = char2; + } + return dni4; + } + + // returns true if regex matches or if there's no regex + scope.validateDni = function(dni) { + var norm_dni = normalize_dni(dni); + + if (!norm_dni.match(dni_re)) { + return true; } - }; - return patterns; -}), angular.module("avRegistration").directive("avrField", [ "$state", function($state) { + + var prefix = norm_dni.charAt(0); + var index = "LMXYZ".indexOf(prefix); + var niePrefix = 0; + if (index > -1) { + niePrefix = index; + norm_dni = norm_dni.substr(1); + if (prefix === 'Y') { + norm_dni = "1" + norm_dni; + } else if (prefix === 'Z') { + norm_dni = "2" + norm_dni; + } + } + var dni_letters = "TRWAGMYFPDXBNJZSQVHLCKE"; + var letter = dni_letters.charAt( parseInt( norm_dni, 10 ) % 23 ); + return letter === norm_dni.charAt(norm_dni.length - 1); + }; + } return { - restrict: "AE", - scope: !0, - link: function(scope, element, attrs) { - console.log("type = " + scope.field.type), scope.index = attrs.index; - }, - templateUrl: "avRegistration/field-directive/field-directive.html" + restrict: 'AE', + link: link, + scope: true, + templateUrl: 'avRegistration/fields/dni-field-directive/dni-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrEmailField", [ "$state", "Patterns", function($state, Patterns) { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrCodeField', function($state, Plugins) { + function link(scope, element, attrs) { + scope.codePattern = /[abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789-]{8,9}/; + var rand_code = '' + _.random(1e12); + scope.code_id = 'input' + scope.index + rand_code; + + scope.showResendAuthCode = function () + { + var data = {showUserSendAuthCode: true}; + Plugins.hook('hide-user-send-auth-code', data); + return data.showUserSendAuthCode; + }; + + // TODO: validate email for email-otp. For now, we just allow the resend + // button for that use-case + if (_.contains(['sms', 'sms-otp'], scope.method)) { + var telInput = + angular.element(document.getElementById('input' + scope.telIndex)); + scope.isValidTel = telInput.intlTelInput("isValidNumber"); + scope.$watch('telField.value', + function (newValue, oldValue) { + scope.isValidTel = telInput.intlTelInput("isValidNumber"); + }, + true); + } + } return { - restrict: "AE", - link: function(scope, element, attrs) { - scope.emailRe = Patterns.get("email"); - }, - scope: !0, - templateUrl: "avRegistration/fields/email-field-directive/email-field-directive.html" + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avRegistration/fields/code-field-directive/code-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrDateField", [ "$state", "Patterns", function($state, Patterns) { - return { - restrict: "AE", - link: function(scope, element, attrs) { - var data, dateValue; - function numberPadStart(str, size) { - str = "000000000" + str; - return str.substr(str.length - size); + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrTelField', function($state, $timeout) { + function link(scope, element, attrs) { + + scope.tlfPattern = /^[+]?\d{9,14}$/; + scope.isValidNumber = true; + + // lookup ip data and send callbacks when it is available + + var ipData = null; + var ipCallbacks = []; + $.get('https://ipinfo.io', function() {}, "jsonp") + .always(function(resp) { + ipData = resp; + for (var i = 0; i < ipCallbacks.length; i++) { + ipCallbacks[i](); + } + }); + + $timeout(function() { + /* configure registration telephone phone number */ + var telInput = angular.element(document.getElementById("input" + scope.index)); + // initialise plugin + telInput.intlTelInput({ + utilsScript: "election/utils.js", + separateDialCode: true, + initialCountry: "auto", + preferredCountries: ["es", "gb", "us"], + autoPlaceholder: "aggressive", + placeholderNumberType: "MOBILE", + geoIpLookup: function(callback) { + var applyCountry = function() + { + var countryCode = (ipData && ipData.country) ? ipData.country : "es"; + callback(countryCode); + }; + if (ipData) { + applyCountry(); + } else { + ipCallbacks.push(applyCountry); + } } - scope.years = [], scope.months = [], scope.field = scope.$parent.field, scope.date = null, - dateValue = (dateValue = null) === scope.field.value || 0 === scope.field.value.length ? new Date() : (data = scope.field.value.split("-"), - new Date(data[0], parseInt(data[1]) - 1, data[2])), scope.date = { - year: dateValue.getFullYear(), - month: dateValue.getMonth() + 1, - day: dateValue.getDate() - }, scope.getYears = function() { - for (var initY = new Date().getFullYear(), i = 0, years = [], i = initY; initY - 130 <= i; i--) years.push(i); - return years; - }, scope.getMonths = function() { - for (var i = 0, months = [], i = 1; i <= 12; i++) months.push(i); - return months; - }, scope.getDays = function() { - for (var days = [], i = 0, ndays = new Date(scope.date.year, scope.date.month, 0).getDate(), i = 1; i <= ndays; i++) days.push(i); - return days; - }, scope.onChange = function() { - var monthStr = numberPadStart(scope.date.month, 2), dayStr = numberPadStart(scope.date.day, 2); - scope.field.value = scope.date.year + "-" + monthStr + "-" + dayStr; - }, scope.onChange(); - }, - scope: { - label: "=" - }, - templateUrl: "avRegistration/fields/date-field-directive/date-field-directive.html" - }; -} ]), angular.module("avRegistration").directive("avrPasswordField", [ "$state", function($state) { - return { - restrict: "AE", - scope: !0, - templateUrl: "avRegistration/fields/password-field-directive/password-field-directive.html" - }; -} ]), angular.module("avRegistration").directive("avrTextField", [ "$state", function($state) { + }); + if (_.isString(scope.field.value) && 0 < scope.field.value.length) { + telInput.intlTelInput("setNumber", scope.field.value); + } + + var validateTel = function() + { + scope.$evalAsync(function() { + var intlNumber = telInput.intlTelInput("getNumber"); + if (intlNumber) { + scope.field.value = intlNumber; + } + var isValid = telInput.intlTelInput("isValidNumber"); + if (!isValid && $("#input"+ scope.index).val().replace("[ \t\n]", "").length > 0) + { + telInput.toggleClass("error", true); + scope.isValidNumber = false; + } else + { + telInput.toggleClass("error", false); + scope.isValidNumber = true; + } + }); + }; + // on keyup / change flag: reset + telInput.on("keyup change", validateTel); + }); + } return { - restrict: "AE", - link: function(scope, element, attrs) { - angular.isUndefined(scope.field.regex) ? scope.re = new RegExp("") : scope.re = new RegExp(scope.field.regex); - }, - scope: !0, - templateUrl: "avRegistration/fields/text-field-directive/text-field-directive.html" + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avRegistration/fields/tel-field-directive/tel-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrDniField", [ "$state", function($state) { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrBoolField', function($state) { return { - restrict: "AE", - link: function(scope, element, attrs) { - var dni_re = /^([0-9]{1,8}[A-Z]|[LMXYZ][0-9]{1,7}[A-Z])$/; - scope.validateDni = function(index) { - var norm_dni = function(dni) { - if (!dni) return ""; - for (var dni2 = dni.toUpperCase(), dni3 = "", i = 0; i < dni2.lenth; i++) { - var char = dni2[i]; - 0 <= "QWERTYUIOPASDFGHJKLZXCVBNM1234567890".indexOf(char) && (dni3 += char); - } - for (var last_char = "", dni4 = "", j = 0; j < dni3.lenth; j++) { - var char2 = dni3[j]; - "" === last_char || "1234567890".indexOf(last_char), dni4 += char2, last_char = char2; - } - return dni4; - }(index); - if (!norm_dni.match(dni_re)) return !0; - var prefix = norm_dni.charAt(0), index = "LMXYZ".indexOf(prefix); - -1 < index && (norm_dni = norm_dni.substr(1), "Y" === prefix ? norm_dni = "1" + norm_dni : "Z" === prefix && (norm_dni = "2" + norm_dni)); - return "TRWAGMYFPDXBNJZSQVHLCKE".charAt(parseInt(norm_dni, 10) % 23) === norm_dni.charAt(norm_dni.length - 1); - }; - }, - scope: !0, - templateUrl: "avRegistration/fields/dni-field-directive/dni-field-directive.html" + restrict: 'AE', + scope: true, + templateUrl: 'avRegistration/fields/bool-field-directive/bool-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrCodeField", [ "$state", "Plugins", function($state, Plugins) { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrIntField', function($state) { + function link(scope, element, attrs) { + if (angular.isUndefined(scope.field.regex)) { + scope.re = new RegExp(""); + } else { + scope.re = new RegExp(scope.field.regex); + } + } return { - restrict: "AE", - scope: !0, - link: function(scope, element, attrs) { - scope.codePattern = /[abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789-]{8,9}/; - var telInput, rand_code = "" + _.random(1e12); - scope.code_id = "input" + scope.index + rand_code, scope.showResendAuthCode = function() { - var data = { - showUserSendAuthCode: !0 - }; - return Plugins.hook("hide-user-send-auth-code", data), data.showUserSendAuthCode; - }, _.contains([ "sms", "sms-otp" ], scope.method) && (telInput = angular.element(document.getElementById("input" + scope.telIndex)), - scope.isValidTel = telInput.intlTelInput("isValidNumber"), scope.$watch("telField.value", function(newValue, oldValue) { - scope.isValidTel = telInput.intlTelInput("isValidNumber"); - }, !0)); - }, - templateUrl: "avRegistration/fields/code-field-directive/code-field-directive.html" + restrict: 'AE', + link: link, + scope: true, + templateUrl: 'avRegistration/fields/int-field-directive/int-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrTelField", [ "$state", "$timeout", function($state, $timeout) { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrCaptchaField', ['Authmethod', '$state', '$interval', function(Authmethod, $state, $interval) { + function link(scope, element, attrs) { + var timeoutId = null; + + scope.authMethod = Authmethod; + Authmethod.newCaptcha(""); + } + return { - restrict: "AE", - scope: !0, - link: function(scope, element, attrs) { - scope.tlfPattern = /^[+]?\d{9,14}$/, scope.isValidNumber = !0; - var ipData = null, ipCallbacks = []; - $.get("https://ipinfo.io", function() {}, "jsonp").always(function(resp) { - ipData = resp; - for (var i = 0; i < ipCallbacks.length; i++) ipCallbacks[i](); - }), $timeout(function() { - var telInput = angular.element(document.getElementById("input" + scope.index)); - telInput.intlTelInput({ - utilsScript: "election/utils.js", - separateDialCode: !0, - initialCountry: "auto", - preferredCountries: [ "es", "gb", "us" ], - autoPlaceholder: "aggressive", - placeholderNumberType: "MOBILE", - geoIpLookup: function(callback) { - function applyCountry() { - var countryCode = ipData && ipData.country ? ipData.country : "es"; - callback(countryCode); - } - ipData ? applyCountry() : ipCallbacks.push(applyCountry); - } - }), _.isString(scope.field.value) && 0 < scope.field.value.length && telInput.intlTelInput("setNumber", scope.field.value); - telInput.on("keyup change", function() { - scope.$evalAsync(function() { - var intlNumber = telInput.intlTelInput("getNumber"); - intlNumber && (scope.field.value = intlNumber), !telInput.intlTelInput("isValidNumber") && 0 < $("#input" + scope.index).val().replace("[ \t\n]", "").length ? (telInput.toggleClass("error", !0), - scope.isValidNumber = !1) : (telInput.toggleClass("error", !1), scope.isValidNumber = !0); - }); - }); - }); - }, - templateUrl: "avRegistration/fields/tel-field-directive/tel-field-directive.html" + restrict: 'AE', + scope: true, + link: link, + templateUrl: 'avRegistration/fields/captcha-field-directive/captcha-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrBoolField", [ "$state", function($state) { + }]); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrTextareaField', function($state) { return { - restrict: "AE", - scope: !0, - templateUrl: "avRegistration/fields/bool-field-directive/bool-field-directive.html" + restrict: 'AE', + scope: true, + templateUrl: 'avRegistration/fields/textarea-field-directive/textarea-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrIntField", [ "$state", function($state) { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avRegistration') + .directive('avrImageField', function($state, $timeout) { + function link(scope, element, attrs) { + function readImage(input) { + if ( input.files && input.files[0] ) { + var FR = new FileReader(); + FR.onload = function(e) { + scope.field.value = e.target.result; + }; + FR.readAsDataURL( input.files[0] ); + } + } + + $timeout(function() { + $("#image-field").change(function() { readImage( this ); }); + }, 0); + } + return { - restrict: "AE", - link: function(scope, element, attrs) { - angular.isUndefined(scope.field.regex) ? scope.re = new RegExp("") : scope.re = new RegExp(scope.field.regex); - }, - scope: !0, - templateUrl: "avRegistration/fields/int-field-directive/int-field-directive.html" + restrict: 'AE', + link: link, + scope: true, + templateUrl: 'avRegistration/fields/image-field-directive/image-field-directive.html' }; -} ]), angular.module("avRegistration").directive("avrCaptchaField", [ "Authmethod", "$state", "$interval", function(Authmethod, $state, $interval) { - return { - restrict: "AE", - scope: !0, - link: function(scope, element, attrs) { - (scope.authMethod = Authmethod).newCaptcha(""); + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/** + * @description Service that manages the Plugins extension points. + * + * These are the hooks called by agora-gui-admin: + * + * - Name: election-modified + * + * Description: called by @a ElectionsApi.setCurrent service before the new + * election is set. + * + * Input data: { + * // old election object (before setCurrent was called) + * "old": Election, + * + * // old new election object that is going to be set + * "el": Election + * } + * + * - Name: send-auth-codes-steps + * + * Description: called by @a SendMsg.calculateSteps service before calculating + * the number of steps of the send authentication codes dialog. It's a good + * way of modifying @a SendMsg.steps.extra. + * + * Input data: { + * // current election object + * "el": Election, + * + * // ids of the electorate to which the authentication message is going + * // to be set. Might be null if it's all the electorate. + * "user_ids": List[Integer] + * } + * + * - Name: send-auth-codes-confirm-extra + * + * Description: called by @a SendMsg.confirmAuthCodesModal service before + * showing the @a SendAuthCodesModalConfirm window when sending authentication + * codes to the electorate. This hook allows to set some html to be shown in + * the modal window. Note that the html will not be trusted unless you + * explicitly make it trusted with @a $sce. + * + * Input data: { + * // modifiable list of html strings to shown in the modal confirm window. + * // starts empty, but other hook handlers might modify it. It's used as + * // the hook's output. + * "html": [] + * } + * + * - Name: send-auth-codes-confirm-close + * + * Description: Called by @a .confirmAuthCodesModal service after + * closing the @a SendAuthCodesModalConfirm window to process the result of + * the modal (this result is the input of the hook) and decide what to do. + * + * Input data: string + * + * - Name: send-auth-codes-pre + * + * Description: Called by @a SendMsg.sendAuthCodes before sending auth codes. + * Used to decide whether or not to send them - if any hook handler returns + * a value interpretable as false, won't send it. + * + * Input data: { + * // current election object + * "el": Election, + * + * // ids of the electorate to which the authentication message is going + * // to be set. Might be null if it's all the electorate. + * "user_ids": List[Integer] + * } + * + * - Name: send-auth-codes-success + * + * Description: Called by @a SendMsg.sendAuthCodes after sending auth codes + * when the sending was successful. + * + * Input data: { + * // current election object + * "el": Election, + * + * // ids of the electorate to which the authentication message is going + * // to be set. Might be null if it's all the electorate. + * "ids": List[Integer] + * + * // response object from jquery + * "response": ResponseObject + * } + * + * - Name: send-auth-codes-error + * + * Description: Called by @a SendMsg.sendAuthCodes after sending auth codes + * when the sending had an error. + * + * Input data: { + * // current election object + * "el": Election, + * + * // ids of the electorate to which the authentication message is going + * // to be set. Might be null if it's all the electorate. + * "ids": List[Integer] + * + * // response object from jquery + * "response": ResponseObject + * + * - Name: add-to-census-pre + * + * Description: Called by @a avAdminElcensus.censusCall just before adding + * some electors to the election. A hook handler can cancel the add to census + * action return a value interpretable as false. + * + * // List of electors that are about to be added + * Input data: List[NewElectorMetadata] + * + * - Name: add-to-census-success + * + * Description: Called by @a avAdminElcensus.censusCall after adding + * some electors to the election when the call to the API was successful. + * Allows the hook handler process the api result. + * + * Input data: { + * // List of electors that are about to be added + * "data": List[NewElectorMetadata], + * + * // response object from jquery + * "response": ResponseObject + * } + * + * - Name: add-to-census-error + * + * Description: Called by @a avAdminElcensus.censusCall after adding + * some electors to the election when the call to the api produced an error. + * Allows the hook handler process the api result. + * + * Input data: { + * // List of electors that are about to be added + * "data": List[NewElectorMetadata], + * + * // response object from jquery + * "response": ResponseObject + * } + */ +angular.module('avRegistration') + .factory('Plugins', function() { + var plugins = {}; + // TODO: What are plugins used for exactly? Please explain + plugins.plugins = {list: []}; + + // Signal storage + plugins.signals = $.Callbacks("unique"); + + /** + * List of hooks handlers. + * + * A hook is a point of extension. Each time @a Plugins.hook() + * is called, all the hooks are called with the arguments given and in + * list order, so that they can process the hook. + * + * To insert/delete/list hook handlers, access directly to + * @a Plugins.hooks. + * + * Each hook handler is a function that receives two arguments: + * - hookname + * - data + * + * A hook handler should return a value interpretable as a false + * expression if it wants no other hook to process the call, or + * anything else otherwise. + * + * Example hook handler: + * + * + * var fooHookHandler = function(hookname, data) { + * if (hookname === "foo") { + * processFoo(data); + * return false; + * } + * + * return true; + * }; + * + * // add the handler + * Plugins.hooks.push(fooHookHandler); + * + */ + plugins.hooks = []; + + /* + * Adds a plugin. + * + * plugin format: + * { + * name: 'test', + * directive: 'test', (optional, only if this link has a directive) + * head: true | false, + * link: ui-sref link, + * menu: html() | {icon: icon, text: text} + * } + */ + plugins.add = function(plugin) { + plugins.plugins.list.push(plugin); + }; + + /* + * Clears the plugins list. + */ + plugins.clear = function() { + plugins.plugins.list = []; + }; + + /** + * Remove a plugin from the list. + */ + plugins.remove = function(plugin) { + // Implemented by creating a new list without the plugin of that + // name + var pluginList = plugins.plugins.list; + plugins.plugins.list = []; + pluginList.forEach(function(pluginFromList) { + if (plugin.name !== pluginFromList.name) { + plugins.plugins.list.push(pluginFromList); + } + }); + }; + + /** + * Emits a signal by name. + * + * @data can be any object or even null. + */ + plugins.emit = function(signalName, data) { + plugins.signals.fire(signalName, data); + }; + + /** + * Calls to a hook by name. + * + * Each function stored as a hook is called with the provided + * @a hookname and @a data in the hook insertion order. When a hook + * returns a value interpretable as false, no more hooks are called. + * + * @a data can be any object or even null. + * @a hookname should be a string. + * + * @returns false if any of the hooks returns false, or true otherwise. + */ + plugins.hook = function(hookname, data) { + for (var i=0; i + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * The avUi module contains a series of user interface directives and utilities. + */ + +angular.module('avUi', []); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +jQuery.fn.flash = function(duration) { + var selector = this; + + if (!angular.isNumber(duration)) { + duration = 300; + } + + if (selector.attr("is-flashing") === "true") { + return; + } + + selector.attr("is-flashing", "true"); + + selector + .addClass("flashing") + .delay(duration) + .queue(function() { + selector.removeClass("flashing").addClass("flashing-out").dequeue(); + }) + .delay(duration) + .queue(function() { + selector.removeClass("flashing flashing-out").dequeue(); + selector.attr("is-flashing", "false"); + }); +}; +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2020 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avUi') + .directive( + 'avChildrenElections', + function(ConfigService) + { + // we use it as something similar to a controller here + function link(scope, element, attrs) + { + scope.electionsById = {}; + scope.selectedElectionId = scope.parentElectionId; + + // process each election + _.each( + scope.childrenElectionInfo.presentation.categories, + function (category) + { + _.each( + category.events, + function (election) + { + if ( + scope.mode === 'checkbox' || + scope.mode === 'toggle-and-callback' + ) + { + election.data = election.data || false; + election.disabled = election.disabled || false; + } + } + ); + } + ); + + // add a processElection function + scope.click = function (election) + { + console.log("click to election.event_id = " + election.event_id); + if (scope.mode === 'checkbox') + { + election.data = !election.data; + } + else if (scope.mode === 'toggle-and-callback') + { + scope.selectedElectionId = election.event_id; + scope.callback({electionId: election.event_id}); + } + }; + } + + return { + restrict: 'AE', + scope: { + mode: '@', + callback: '&?', + parentElectionId: '@?', + childrenElectionInfo: '=' }, - scope: !0, - templateUrl: "avRegistration/fields/image-field-directive/image-field-directive.html" - }; -} ]), angular.module("avRegistration").factory("Plugins", function() { - var plugins = { - plugins: { - list: [] + link: link, + templateUrl: 'avUi/children-elections-directive/children-elections-directive.html' + }; + } + ); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * Simple error directive. + */ +angular.module('avUi') + .directive('avSimpleError', function($resource, $window) { + function link(scope, element, attrs) { + // moves the title on top of the busy indicator + scope.updateTitle = function() { + var title = element.find(".av-simple-error-title"); + + // set margin-top + var marginTop = - title.height() - 45; + var marginLeft = - title.width()/2; + title.attr("style", "margin-top: " + marginTop + "px; margin-left: " + marginLeft + "px"); + }; + + scope.$watch(attrs.title, + function() { + scope.updateTitle(); } - }; - return plugins.signals = $.Callbacks("unique"), plugins.hooks = [], plugins.add = function(plugin) { - plugins.plugins.list.push(plugin); - }, plugins.clear = function() { - plugins.plugins.list = []; - }, plugins.remove = function(plugin) { - var pluginList = plugins.plugins.list; - plugins.plugins.list = [], pluginList.forEach(function(pluginFromList) { - plugin.name !== pluginFromList.name && plugins.plugins.list.push(pluginFromList); - }); - }, plugins.emit = function(signalName, data) { - plugins.signals.fire(signalName, data); - }, plugins.hook = function(hookname, data) { - for (var i = 0; i < plugins.hooks.length; i++) if (!(0, plugins.hooks[i])(hookname, data)) return !1; - return !0; - }, plugins; -}), angular.module("avRegistration").directive("avPluginHtml", [ "$compile", "$sce", "$parse", function($compile, $sce, $parse) { - return function(scope, element, attrs) { - var parsedHtml = $parse(attrs.ngBindHtml); - scope.$watch(function() { - return (parsedHtml(scope) || "").toString(); - }, function() { - $compile(element, null, -9999)(scope); - }); - }; -} ]), angular.module("avUi", []), jQuery.fn.flash = function(duration) { - var selector = this; - angular.isNumber(duration) || (duration = 300), "true" !== selector.attr("is-flashing") && (selector.attr("is-flashing", "true"), - selector.addClass("flashing").delay(duration).queue(function() { - selector.removeClass("flashing").addClass("flashing-out").dequeue(); - }).delay(duration).queue(function() { - selector.removeClass("flashing flashing-out").dequeue(), selector.attr("is-flashing", "false"); - })); -}, angular.module("avUi").directive("avChildrenElections", [ "ConfigService", function(ConfigService) { + ); + } return { - restrict: "AE", - scope: { - mode: "@", - callback: "&?", - parentElectionId: "@?", - childrenElectionInfo: "=" - }, - link: function(scope, element, attrs) { - scope.electionsById = {}, scope.selectedElectionId = scope.parentElectionId, _.each(scope.childrenElectionInfo.presentation.categories, function(category) { - _.each(category.events, function(election) { - "checkbox" !== scope.mode && "toggle-and-callback" !== scope.mode || (election.data = election.data || !1, - election.disabled = election.disabled || !1); - }); - }), scope.click = function(election) { - console.log("click to election.event_id = " + election.event_id), "checkbox" === scope.mode ? election.data = !election.data : "toggle-and-callback" === scope.mode && (scope.selectedElectionId = election.event_id, - scope.callback({ - electionId: election.event_id - })); - }; - }, - templateUrl: "avUi/children-elections-directive/children-elections-directive.html" + restrict: 'AE', + scope: {}, + link: link, + transclude: true, + templateUrl: 'avUi/simple-error-directive/simple-error-directive.html' }; -} ]), angular.module("avUi").directive("avSimpleError", [ "$resource", "$window", function($resource, $window) { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * Simple change lang directive, that can be used in the navbar as a list + * element: + * + */ +angular.module('avUi') + .directive('avChangeLang', function($i18next, ipCookie, angularLoad, amMoment, ConfigService) { + function link(scope, element, attrs) { + scope.deflang = window.i18n.lng(); + angular.element('#ng-app').attr('lang', scope.deflang); + scope.langs = $i18next.options.lngWhitelist; + + // Changes i18n to a specific language, setting also a cookie for + // remembering it, and updating all the translations instantly. + // + // Triggered when the user clicks and selects a language. + scope.changeLang = function(lang) { + $i18next.options.lng = lang; + console.log("setting cookie"); + var cookieConf = { + expires: 360, + path: "/" + }; + ipCookie( + "lang", + lang, + _.extend(cookieConf, ConfigService.i18nextCookieOptions)); + scope.deflang = lang; + angular.element('#ng-app').attr('lang', scope.deflang); + + // async load moment i18n + angularLoad + .loadScript(ConfigService.base + '/locales/moment/' + lang + '.js') + .then(function () { + amMoment.changeLocale(lang); + }); + }; + } + return { - restrict: "AE", - scope: {}, - link: function(scope, element, attrs) { - scope.updateTitle = function() { - var title = element.find(".av-simple-error-title"), marginTop = -title.height() - 45, marginLeft = -title.width() / 2; - title.attr("style", "margin-top: " + marginTop + "px; margin-left: " + marginLeft + "px"); - }, scope.$watch(attrs.title, function() { - scope.updateTitle(); - }); - }, - transclude: !0, - templateUrl: "avUi/simple-error-directive/simple-error-directive.html" + restrict: 'AE', + scope: {}, + link: link, + templateUrl: 'avUi/change-lang-directive/change-lang-directive.html' }; -} ]), angular.module("avUi").directive("avChangeLang", [ "$i18next", "ipCookie", "angularLoad", "amMoment", "ConfigService", function($i18next, ipCookie, angularLoad, amMoment, ConfigService) { - return { - restrict: "AE", - scope: {}, - link: function(scope, element, attrs) { - scope.deflang = window.i18n.lng(), angular.element("#ng-app").attr("lang", scope.deflang), - scope.langs = $i18next.options.lngWhitelist, scope.changeLang = function(lang) { - $i18next.options.lng = lang, console.log("setting cookie"); - ipCookie("lang", lang, _.extend({ - expires: 360, - path: "/" - }, ConfigService.i18nextCookieOptions)), scope.deflang = lang, angular.element("#ng-app").attr("lang", scope.deflang), - angularLoad.loadScript(ConfigService.base + "/locales/moment/" + lang + ".js").then(function() { - amMoment.changeLocale(lang); - }); - }; - }, - templateUrl: "avUi/change-lang-directive/change-lang-directive.html" + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * directive used to position an element always at the bottom, so that it's + * always shown completely. There are two scenarios: + * a) if the page has no scroll, we assume the element is shown, and do nothing + * b) if the page has scroll, the bottom of the page is not completely (or at + * all) being shown, so we set the affixed element the class affix-bottom and + * make space for it giving some bottom margin in its parent element. + * + * As an optional trigger to the settings of the affix-bottom, you can also set + * the data-force-affix-width attribute in the affixed element to a number of + * pixels. If this attribute is set and the window width is less than this, + * automatically the element will be affixed. + */ +angular.module('avUi') + .directive('avAffixBottom', function($window, $timeout, $parse) { + var affixBottomClass = "affix-bottom"; + var checkPosition = function(scope, instance, el, options) { + + var affix = false; + var elHeight = $(el).actual('height'); + + if (($("body").height() + elHeight > window.innerHeight) || + (instance.forceAffixWidth && window.innerWidth < instance.forceAffixWidth)) { + affix = affixBottomClass; + } + + if (instance.affixed === affix) { + return; + } + + instance.affix = affix; + instance.setIsAffix(scope, affix); + el.removeClass("hidden"); + + if (!affix) { + el.removeClass(affixBottomClass); + $(el).parent().css("margin-bottom", instance.defaultBottomMargin); + } else { + el.addClass(affixBottomClass); + + // add bottom-margin automatically + $(el).parent().css("margin-bottom", elHeight + "px"); + } + }; -} ]), angular.module("avUi").directive("avAffixBottom", [ "$window", "$timeout", "$parse", function($window, $timeout, $parse) { + return { - restrict: "EAC", - link: function(scope, iElement, iAttrs) { - var timeout, instance = { - affix: !1, - getIsAffix: null, - setIsAffix: angular.noop, - defaultBottomMargin: iElement.css("margin-bottom"), - forceAffixWidth: parseInt(iAttrs.forceAffixWidth, 10) - }; - function callCheckPos() { - timeout = $timeout(function() { - $timeout.cancel(timeout), function(scope, instance, el) { - var affix = !1, elHeight = $(el).actual("height"); - ($("body").height() + elHeight > window.innerHeight || instance.forceAffixWidth && window.innerWidth < instance.forceAffixWidth) && (affix = "affix-bottom"), - instance.affixed !== affix && (instance.affix = affix, instance.setIsAffix(scope, affix), - el.removeClass("hidden"), affix ? (el.addClass("affix-bottom"), $(el).parent().css("margin-bottom", elHeight + "px")) : (el.removeClass("affix-bottom"), - $(el).parent().css("margin-bottom", instance.defaultBottomMargin))); - }(scope, instance, iElement); - }, 300); - } - 0 < iAttrs.avAffixBottom.length && (instance.getIsAffix = $parse(iAttrs.avAffixBottom), - instance.setIsAffix = instance.getIsAffix.assign), callCheckPos(), angular.element($window).on("resize", callCheckPos), - angular.element(document.body).on("resize", callCheckPos), console.log("iElement NOT resize, height = " + iElement.height()), - angular.element(iElement).on("resize", callCheckPos); + restrict: 'EAC', + link: function(scope, iElement, iAttrs) { + // instance saves state between calls to checkPosition + var instance = { + affix: false, + getIsAffix: null, + setIsAffix: angular.noop, + defaultBottomMargin: iElement.css("margin-bottom"), + forceAffixWidth: parseInt(iAttrs.forceAffixWidth, 10) + }; + + + if (iAttrs.avAffixBottom.length > 0) { + instance.getIsAffix = $parse(iAttrs.avAffixBottom); + instance.setIsAffix = instance.getIsAffix.assign; + } + + // timeout is used with callCheckPos so that we do not create too many + // calls to checkPosition, at most one per 300ms + var timeout; + + function callCheckPos() { + timeout = $timeout(function() { + $timeout.cancel(timeout); + checkPosition(scope, instance, iElement, iAttrs); + }, 300); } + callCheckPos(); + + // watch for window resizes and element resizes too + angular.element($window).on('resize', callCheckPos); + angular.element(document.body).on('resize', callCheckPos); + console.log("iElement NOT resize, height = " + iElement.height()); + angular.element(iElement).on('resize', callCheckPos); + } }; -} ]), angular.module("avUi").directive("avAutoHeight", [ "$window", "$timeout", function($window, $timeout) { + + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/** + * Usage: + * + *
+ *
I need some space, this is a sibling
+ *
+ * I stretch to the available height, + * calculated from the height available from .parent and my siblings. + *
+ *
+ */ +angular.module('avUi') + .directive('avAutoHeight', function($window, $timeout) { return { - link: function(scope, element, attrs) { - var promise = null, sibling = function() { - return element.closest(attrs.parentSelector).find(attrs.siblingSelector); - }, recalculate = function() { - promise && $timeout.cancel(promise), promise = $timeout(function() { - var height, additionalHeight = 0; - attrs.additionalHeight && (additionalHeight = parseInt(attrs.additionalHeight, 10)), - height = sibling().height(), element.css("max-height", height + additionalHeight + "px"); - }, 300); - }; - scope.$watch(function() { - return sibling().height(); - }, function(newValue, oldValue) { - recalculate(); - }), recalculate(); + link: function(scope, element, attrs) { + var sibling, recalculate, promise = null; + + sibling = function() { + return element.closest(attrs.parentSelector).find(attrs.siblingSelector); + }; + + recalculate = function () { + if (promise) { + $timeout.cancel(promise); + } + promise = $timeout(function() { + var additionalHeight = 0, height; + if (!!attrs.additionalHeight) { + additionalHeight = parseInt(attrs.additionalHeight, 10); + } + height = sibling().height(); + element.css('max-height', (height + additionalHeight) + "px"); + }, 300); + }; + + scope.$watch( + function () { + return sibling().height(); + }, + function (newValue, oldValue) { + recalculate(); + }); + + recalculate(); + } + }; + } +); +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avUi') + .directive('avAffixTopOffset', function($window, $timeout, $parse) { + var affixClass = "affix-top"; + var checkPosition = function(scope, instance, el, options) { + + var affix = false; + var offset = el.offset(); + + if (instance.affix && $window.pageYOffset + 20 >= instance.scrollAffix) { + return; + } else if (offset.top - $window.pageYOffset < instance.avAffixTopOffset) { + affix = true; + } + + if (instance.affix === affix) { + return; + } + + instance.affix = affix; + instance.scrollAffix = $window.pageYOffset; + if (!affix) { + el.removeClass(affixClass); + el.attr("style", ""); + + if (options.affixPlaceholder !== undefined) { + $(options.affixPlaceholder).removeClass("affixed"); + } + } else { + el.addClass(affixClass); + el.data("page-offset", $window.pageYOffset); + el.css("position", "fixed"); + el.css("float", "none"); + el.css("top", Math.floor(instance.avAffixTopOffset) + "px"); + el.css("left", Math.floor(instance.baseOffset.left) + "px"); + el.css("width", Math.floor(instance.baseWidth) + "px"); + el.css( "z-index", "10"); + + if (options.affixPlaceholder !== undefined) { + $(options.affixPlaceholder).addClass("affixed"); } + } + }; -} ]), angular.module("avUi").directive("avAffixTopOffset", [ "$window", "$timeout", "$parse", function($window, $timeout, $parse) { + return { - restrict: "EAC", - link: function(scope, iElement, iAttrs) { - var instance = { - affix: !1, - scrollAffix: null, - baseOffset: iElement.offset(), - baseWidth: iElement.width(), - avAffixTopOffset: parseInt(iAttrs.avAffixTopOffset, 10) - }; - function callCheckPos() { - !function(instance, el, options) { - var affix = !1, offset = el.offset(); - instance.affix && $window.pageYOffset + 20 >= instance.scrollAffix || (offset.top - $window.pageYOffset < instance.avAffixTopOffset && (affix = !0), - instance.affix !== affix && (instance.affix = affix, instance.scrollAffix = $window.pageYOffset, - affix ? (el.addClass("affix-top"), el.data("page-offset", $window.pageYOffset), - el.css("position", "fixed"), el.css("float", "none"), el.css("top", Math.floor(instance.avAffixTopOffset) + "px"), - el.css("left", Math.floor(instance.baseOffset.left) + "px"), el.css("width", Math.floor(instance.baseWidth) + "px"), - el.css("z-index", "10"), void 0 !== options.affixPlaceholder && $(options.affixPlaceholder).addClass("affixed")) : (el.removeClass("affix-top"), - el.attr("style", ""), void 0 !== options.affixPlaceholder && $(options.affixPlaceholder).removeClass("affixed")))); - }(instance, iElement, iAttrs); - } - callCheckPos(), angular.element($window).on("scroll", callCheckPos), angular.element($window).on("resize", function() { - iElement.removeClass("affix-top"), iElement.attr("style", ""), instance.affix = !1, - instance.scrollAffix = null, $timeout(function() { - instance.baseOffset = iElement.offset(), instance.baseWidth = iElement.width(), - callCheckPos(); - }, 300); - }); + restrict: 'EAC', + link: function(scope, iElement, iAttrs) { + // instance saves state between calls to checkPosition + var instance = { + affix: false, + scrollAffix: null, + baseOffset: iElement.offset(), + baseWidth: iElement.width(), + avAffixTopOffset: parseInt(iAttrs.avAffixTopOffset, 10) + }; + + + function callCheckPos() { + checkPosition(scope, instance, iElement, iAttrs); } + callCheckPos(); + + // when window resizes, the baseoffset etc needs to be reset + function resize() { + iElement.removeClass(affixClass); + iElement.attr("style", ""); + instance.affix = false; + instance.scrollAffix = null; + $timeout(function () { + instance.baseOffset = iElement.offset(); + instance.baseWidth = iElement.width(); + callCheckPos(); + }, 300); + } + + // watch for window scrolling + angular.element($window).on('scroll', callCheckPos); + angular.element($window).on('resize', resize); + } }; -} ]), angular.module("avUi").directive("avAffixTop", [ "$window", "$timeout", function($window, $timeout) { - function updateMargin(el, options) { - var height = parseInt(options.minHeight), height = Math.max($(el).height(), angular.isNumber(height) && !isNaN(height) ? height : 0); - $(options.avAffixTop).css("padding-top", height + "px"); - } + + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * directive used to position an element always at the top. It just sets its + * specified element with a margin-top to make space for the affixed element. + * This is done dynamically, so that each time the affixed element's height + * changes, the top-margin of the specified is recalculated and set. + */ +angular.module('avUi') + .directive('avAffixTop', function($window, $timeout) { + + // add margin-top automatically + var updateMargin = function(el, options) { + var minHeight = parseInt(options.minHeight); + var height = Math.max( + $(el).height(), + (angular.isNumber(minHeight) && !isNaN(minHeight) ? minHeight : 0) ); + $(options.avAffixTop).css("padding-top", height + "px"); + }; + return { - restrict: "EAC", - link: function(scope, iElement, iAttrs) { - var timeout; - function updateMarginTimeout() { - timeout = $timeout(function() { - $timeout.cancel(timeout), updateMargin(iElement, iAttrs); - }, 300); - } - updateMargin(iElement, iAttrs), void 0 === iAttrs.minHeight && (iAttrs.minHeight = "20"), - updateMarginTimeout(), angular.element(iElement).bind("resize", updateMarginTimeout), - angular.element($window).bind("resize", updateMarginTimeout), $(iAttrs.avAffixTop).change(updateMarginTimeout); + restrict: 'EAC', + link: function(scope, iElement, iAttrs) { + updateMargin(iElement, iAttrs); + + if (iAttrs.minHeight === undefined) { + iAttrs.minHeight = "20"; + } + + // timeout is used with callCheckPos so that we do not create too many + // calls to checkPosition, at most one per 300ms + var timeout; + + function updateMarginTimeout() { + timeout = $timeout(function() { + $timeout.cancel(timeout); + updateMargin(iElement, iAttrs); + }, 300); } + updateMarginTimeout(); + + // watch for window resizes and element resizes too + angular.element(iElement).bind('resize', updateMarginTimeout); + angular.element($window).bind('resize', updateMarginTimeout); + $(iAttrs.avAffixTop).change(updateMarginTimeout); + } }; -} ]), angular.module("avUi").directive("avCollapsing", [ "$window", "$timeout", function($window, $timeout) { - function select(instance, el, val) { - val = instance.parentSelector ? el.closest(instance.parentSelector).find(val) : angular.element(val); - return val; + + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * avCollapsing limits the default maximum height of an element by making it + * collapsable if it exceeds the max-height of the selector. + * - if the element's height doesn't exceed its maximum height, the + * data-toggle-selector element will be set to hidden + * - if the element's height exceeds its maximum height, the + * data-toggle-selector element will be removed the class "hidden". + * - if the data-toggle-selector element it contains is clicked, they will be + * added the class ".in". + * - if the element's height exceeds its max height and the toggle is not + * ".in", then it adds the ".collapsed" class to the element, and makes sure + * the data-toggle-selector element is not hidden. + * - it will watch the element and window resizes to see if the conditions + * change. + * - both maxHeightSelector and data-toggle-selector will be found using the + * parent selector as a base if the attribute "parent-selector" is set. + * Otherwise, it will directly a global angular.element() to find them. + */ +angular.module('avUi') + .directive('avCollapsing', function($window, $timeout) { + + function select(instance, el, selector) { + var val; + if (!!instance.parentSelector) { + val = el.closest(instance.parentSelector).find(selector); + } else { + val = angular.element(selector); + } + return val; } + function collapseEl(instance, el) { - return instance.collapseSelector ? select(instance, el, instance.collapseSelector) : angular.element(el); + var val = null; + if (!!instance.collapseSelector) { + val = select(instance, el, instance.collapseSelector); + } else { + val = angular.element(el); + } + return val; } - return { - restrict: "EAC", - link: function(scope, iElement, iAttrs) { - var timeout, instance = { - isCollapsed: !1, - maxHeightSelector: iAttrs.avCollapsing, - toggleSelector: iAttrs.toggleSelector, - parentSelector: iAttrs.parentSelector, - collapseSelector: iAttrs.collapseSelector - }; - function callCheck() { - timeout = $timeout(function() { - $timeout.cancel(timeout), function(instance, el) { - var maxHeight = select(instance, el, instance.maxHeightSelector).css("max-height"), height = angular.element(el)[0].scrollHeight, paddingTop = angular.element(el).css("padding-top"); - -1 !== maxHeight.indexOf("px") ? (paddingTop = paddingTop && -1 !== paddingTop.indexOf("px") ? parseInt(paddingTop.replace("px", "")) : 0, - (maxHeight = parseInt(maxHeight.replace("px", ""))) < height - paddingTop ? instance.isCollapsed || (instance.isCollapsed = !0, - collapseEl(instance, el).addClass("collapsed"), select(instance, el, instance.toggleSelector).removeClass("hidden in")) : instance.isCollapsed && (instance.isCollapsed = !1, - collapseEl(instance, el).removeClass("collapsed"), select(instance, el, instance.toggleSelector).addClass("hidden"))) : console.log("invalid non-pixels max-height for " + instance.maxHeightSelector); - }(instance, iElement); - }, 500); - } - callCheck(), angular.element($window).bind("resize", callCheck), angular.element(iElement).bind("resize", callCheck), - angular.element(instance.toggleSelector).bind("click", function() { - !function(instance, el) { - instance.isCollapsed ? (collapseEl(instance, el).removeClass("collapsed"), select(instance, el, instance.toggleSelector).addClass("in")) : (collapseEl(instance, el).addClass("collapsed"), - select(instance, el, instance.toggleSelector).removeClass("in")), instance.isCollapsed = !instance.isCollapsed; - }(instance, iElement); - }); + + var checkCollapse = function(instance, el, options) { + var maxHeight = select(instance, el, instance.maxHeightSelector).css("max-height"); + var height = angular.element(el)[0].scrollHeight; + + // we want to remove padding-top in the calculation + var paddingTop = angular.element(el).css('padding-top'); + + if (maxHeight.indexOf("px") === -1) { + console.log("invalid non-pixels max-height for " + instance.maxHeightSelector); + return; + } + + if (!paddingTop || paddingTop.indexOf("px") === -1) { + paddingTop = 0; + } else { + paddingTop = parseInt(paddingTop.replace("px", "")); + } + + maxHeight = parseInt(maxHeight.replace("px", "")); + + // make sure it's collapsed if it should + if (height - paddingTop > maxHeight) { + // already collapsed + if (instance.isCollapsed) { + return; + } + instance.isCollapsed = true; + collapseEl(instance, el).addClass("collapsed"); + select(instance, el, instance.toggleSelector).removeClass("hidden in"); + + // removed collapsed and hide toggle otherwise + } else { + // already not collapsed + if (!instance.isCollapsed) { + return; } + instance.isCollapsed = false; + collapseEl(instance, el).removeClass("collapsed"); + select(instance, el, instance.toggleSelector).addClass("hidden"); + } + }; + + var toggleCollapse = function(instance, el, options) { + // if it's collapsed, uncollapse + if (instance.isCollapsed) { + collapseEl(instance, el).removeClass("collapsed"); + select(instance, el, instance.toggleSelector).addClass("in"); + + // collapse otherwise + } else { + collapseEl(instance, el).addClass("collapsed"); + select(instance, el, instance.toggleSelector).removeClass("in"); + } + + + instance.isCollapsed = !instance.isCollapsed; }; -} ]), angular.module("avUi").directive("avRecompile", [ "$compile", "$parse", function($compile, $parse) { - "use strict"; + return { - scope: !0, - compile: function(el) { - var template = function(el) { - return angular.element("").append(el.clone()).html(); - }(el); - return function(scope, $el, attrs) { - var stopWatching = scope.$parent.$watch(attrs.avRecompile, function(_new, _old) { - var newEl = attrs.hasOwnProperty("useBoolean"); - (!newEl || _new && "false" !== _new) && (newEl || _new && _new !== _old) && (newEl && $parse(attrs.kcdRecompile).assign(scope.$parent, !1), - newEl = $compile(template)(scope.$parent), $el.replaceWith(newEl), stopWatching(), - scope.$destroy()); - }); - }; + restrict: 'EAC', + link: function(scope, iElement, iAttrs) { + var instance = { + isCollapsed: false, + maxHeightSelector: iAttrs.avCollapsing, + toggleSelector: iAttrs.toggleSelector, + parentSelector: iAttrs.parentSelector, + collapseSelector: iAttrs.collapseSelector + }; + + // timeout is used with callCheck so that we do not create too many + // calls to checkPosition, at most one per 100ms + var timeout; + + function callCheck() { + timeout = $timeout(function() { + $timeout.cancel(timeout); + checkCollapse(instance, iElement, iAttrs); + }, 500); + } + callCheck(); + + + function launchToggle() { + toggleCollapse(instance, iElement, iAttrs); } + + // watch for window resizes and element resizes too + angular.element($window).bind('resize', callCheck); + angular.element(iElement).bind('resize', callCheck); + + // watch toggle's clicking + angular.element(instance.toggleSelector).bind('click', launchToggle); + } }; -} ]), angular.module("avUi").directive("avDebounce", [ "$timeout", function($timeout) { + + }); + +/* +The MIT License (MIT) +Copyright (c) 2014 Kent C. Dodds +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Source: https://github.com/kentcdodds/kcd-angular +Copyright (c) 2014 Kent C. Dodds kent+github@doddsfamily.us + */ + +angular.module('avUi').directive('avRecompile', function($compile, $parse) { + 'use strict'; + function getElementAsHtml(el) { + return angular.element('').append(el.clone()).html(); + } + + return { + scope: true, // required to be able to clear watchers safely + compile: function(el) { + var template = getElementAsHtml(el); + return function link(scope, $el, attrs) { + var stopWatching = scope.$parent.$watch(attrs.avRecompile, function(_new, _old) { + var useBoolean = attrs.hasOwnProperty('useBoolean'); + if ((useBoolean && (!_new || _new === 'false')) || (!useBoolean && (!_new || _new === _old))) { + return; + } + // reset kcdRecompile to false if we're using a boolean + if (useBoolean) { + $parse(attrs.kcdRecompile).assign(scope.$parent, false); + } + + // recompile + var newEl = $compile(template)(scope.$parent); + $el.replaceWith(newEl); + + // Destroy old scope, reassign new scope. + stopWatching(); + scope.$destroy(); + }); + }; + } + }; +}); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +// source: https://gist.github.com/tommaitland/7579618#file-ng-debounce-js +angular.module('avUi') + .directive('avDebounce', function($timeout) { return { - restrict: "A", - require: "ngModel", - priority: 99, - link: function(scope, elm, attr, ngModelCtrl) { - var debounce; - "radio" !== attr.type && "checkbox" !== attr.type && (elm.unbind("input"), elm.bind("input", function() { - $timeout.cancel(debounce), debounce = $timeout(function() { - scope.$apply(function() { - ngModelCtrl.$setViewValue(elm.val()); - }); - }, attr.avDebounce || 500); - }), elm.bind("blur", function() { - scope.$apply(function() { - ngModelCtrl.$setViewValue(elm.val()); - }); - })); + restrict: 'A', + require: 'ngModel', + priority: 99, + link: function(scope, elm, attr, ngModelCtrl) { + if (attr.type === 'radio' || attr.type === 'checkbox') { + return; } + elm.unbind('input'); + var debounce; + + elm.bind('input', function() { + $timeout.cancel(debounce); + debounce = $timeout( function() { + scope.$apply(function() { + ngModelCtrl.$setViewValue(elm.val()); + }); + }, attr.avDebounce || 500); + }); + + elm.bind('blur', function() { + scope.$apply(function() { + ngModelCtrl.$setViewValue(elm.val()); + }); + }); + } }; -} ]), angular.module("avUi").service("InsideIframeService", function() { +}); +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avUi') + .service('InsideIframeService', function() { return function() { - try { - return window.self !== window.top; - } catch (e) { - return !0; - } + try { + return window.self !== window.top; + } catch (e) { + return true; + } }; -}), angular.module("avUi").directive("avLoadCss", function() { - return { - restrict: "AE", + }); + +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2021 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular + .module('avUi') + .directive( + 'avLoadCss', + function() + { + function link(scope, element, _attrs) + { + function updateCss(newValue, oldValue) + { + if (newValue && typeof newValue === 'string' && newValue !== oldValue) + { + element.text(newValue); + } + } + updateCss(scope.css); + scope.$watch("css", updateCss); + } + + return { + restrict: 'AE', scope: { - css: "=" + css: '=' }, - link: function(scope, element, _attrs) { - function updateCss(newValue, oldValue) { - newValue && "string" == typeof newValue && newValue !== oldValue && element.text(newValue); - } - updateCss(scope.css), scope.$watch("css", updateCss); - } - }; -}), angular.module("avUi").service("PercentVotesService", function() { - return function(total_votes, question, over, format) { - function print(num) { - return "str" === format ? num.toFixed(2) + "%" : num; + link: link + }; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * Returns the percentage of votes received by an answer. The base number + * of the percentage that is used depends on the + * "answer_total_votes_percentage" option in the question. + */ +angular.module('avUi') + .service('PercentVotesService', function() { + return function (total_votes, question, over, format) { + if (format === undefined) { + format = "str"; + } + + function print(num) { + if (format === "str") { + return num.toFixed(2) + "%"; + } else { + return num; } - if (void 0 === format && (format = "str"), 0 === total_votes) return print(0); - var base = question.totals.valid_votes + question.totals.null_votes + question.totals.blank_votes; - return "over-valid-votes" === (over = null == over ? question.answer_total_votes_percentage : over) || "over-total-valid-votes" === over ? base = question.totals.valid_votes : "over-total-valid-points" === over && void 0 !== question.totals.valid_points && (base = question.totals.valid_points), - print(100 * total_votes / base); + } + + // special case + if (total_votes === 0) { + return print(0.00); + } + + var base = question.totals.valid_votes + question.totals.null_votes + question.totals.blank_votes; + if (over === undefined || over === null) { + over = question.answer_total_votes_percentage; + } + if ("over-valid-votes" === over || "over-total-valid-votes" === over) { + base = question.totals.valid_votes; + } + else if ("over-total-valid-points" === over && + undefined !== question.totals.valid_points) { + base = question.totals.valid_points; + } + + return print(100*total_votes / base); }; -}), angular.module("avUi").service("CheckerService", function() { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * Checks input data with a list of checks. + * + * Example: + * + var checks = [ + { + check: "array-group", + prefix: "question-", + checks: [ + {check: "is-array", key: "questions"}, + {check: "array-length", key: "questions", min: 1, max: 40}, + { + check: "array-key-group-chain", + key: "questions", + prefix: "question-", + checks: [ + {check: "is-int", key: "min"}, + {check: "is-int", key: "max"}, + {check: "is-int", key: "num_winners"}, + {check: "is-array", key: "answers"}, + {check: "array-length", key: "answers", min: 1, max: 10000}, + {check: "int-size", key: "min", min: 0, max: "$value.max"}, + { + check: "int-size", + key: "max", + min: "$value.min", + max: "$value.answers.length" + }, + { + check: "int-size", + key: "num_winners", + max: "$value.answers.length" + } + ] + } + ] + } + ]; + + scope.errors = []; + CheckerService({ + checks: checks, + data: scope.elections, + onError: function (errorKey, errorData) { + scope.errors.push({ + data: errorData, + key: errorKey + }); + } + }); + */ +angular.module('avUi') + .service('CheckerService', function() { function checker(d) { - function evalValue(code, $value) { - return angular.isString(code) ? eval(code) : code; + + /* + * Used to eval the expressions given by the programmer in the checker + * script + */ + function evalValue(code, $value) { + if (angular.isString(code)) { + /* jshint ignore:start */ + return eval(code); + /* jshint ignore:end */ + } else { + return code; } - function sumStrs(str1, str2) { - var ret = ""; - return angular.isString(str1) && (ret = str1), angular.isString(str2) && (ret += str2), - ret; + } + + function sumStrs(str1, str2) { + var ret = ""; + if (angular.isString(str1)) { + ret = str1; } - function error(errorKey, errorData, postfix) { - angular.extend(errorData, d.errorData), d.onError(_.reduce([ d.prefix, errorKey, postfix ], sumStrs, ""), errorData); + if (angular.isString(str2)) { + ret += str2; } - angular.isUndefined(d.errorData) && (d.errorData = {}); - var ret = _.every(d.checks, function(item) { - var errorData, min, max, itemMin, itemMax, data, extra, prefix, pass = !0, dataToCheck = angular.isDefined(item.key) ? d.data[item.key] : d.data; - return "is-int" === item.check ? (pass = angular.isNumber(dataToCheck, item.postfix)) || error(item.check, { - key: item.key - }, item.postfix) : "is-array" === item.check ? (pass = angular.isArray(dataToCheck, item.postfix)) || error(item.check, { - key: item.key - }, item.postfix) : "lambda" === item.check ? item.validator(dataToCheck) || (errorData = { - key: item.key - }, angular.isUndefined(item.appendOnErrorLambda) || (errorData = item.appendOnErrorLambda(dataToCheck)), - _.isObject(item.append) && _.isString(item.append.key) && !_.isUndefined(item.append.value) && (errorData[item.append.key] = evalValue(item.append.value, item)), - error(item.check, errorData, item.postfix)) : "is-string-if-defined" === item.check ? (pass = angular.isUndefined(dataToCheck) || angular.isString(dataToCheck, item.postfix)) || error(item.check, { - key: item.key - }, item.postfix) : "array-length-if-defined" === item.check ? angular.isDefined(dataToCheck) && (itemMin = evalValue(item.min, d.data), - itemMax = evalValue(item.max, d.data), (angular.isArray(dataToCheck) || angular.isString(dataToCheck)) && (min = angular.isUndefined(item.min) || dataToCheck.length >= itemMin, - max = angular.isUndefined(item.max) || dataToCheck.length <= itemMax, pass = min && max, - min || error("array-length-min", { - key: item.key, - min: itemMin, - num: dataToCheck.length - }, item.postfix), max || error("array-length-max", { - key: item.key, - max: itemMax, - num: dataToCheck.length - }, item.postfix))) : "is-string" === item.check ? (pass = angular.isString(dataToCheck, item.postfix)) || error(item.check, { - key: item.key - }, item.postfix) : "array-length" === item.check ? (itemMin = evalValue(item.min, d.data), - itemMax = evalValue(item.max, d.data), (angular.isArray(dataToCheck) || angular.isString(dataToCheck)) && (min = angular.isUndefined(item.min) || dataToCheck.length >= itemMin, - max = angular.isUndefined(item.max) || dataToCheck.length <= itemMax, pass = min && max, - min || error("array-length-min", { - key: item.key, - min: itemMin, - num: dataToCheck.length - }, item.postfix), max || error("array-length-max", { - key: item.key, - max: itemMax, - num: dataToCheck.length - }, item.postfix))) : "int-size" === item.check ? (itemMin = evalValue(item.min, d.data), - itemMax = evalValue(item.max, d.data), min = angular.isUndefined(item.min) || itemMin <= dataToCheck, - max = angular.isUndefined(item.max) || dataToCheck <= itemMax, pass = min && max, - min || error("int-size-min", { - key: item.key, - min: itemMin, - value: dataToCheck - }, item.postfix), max || error("int-size-max", { - key: item.key, - max: itemMax, - value: dataToCheck - }, item.postfix)) : "group-chain" === item.check ? pass = _.all(_.map(item.checks, function(check) { + return ret; + } + + function error(errorKey, errorData, postfix) { + angular.extend(errorData, d.errorData); + d.onError( + _.reduce([d.prefix, errorKey, postfix], sumStrs, ""), + errorData + ); + } + + if (angular.isUndefined(d.errorData)) { + d.errorData = {}; + } + + var ret = _.every(d.checks, function (item) { + var pass = true; + var itemMin; + var itemMax; + var max; + var min; + var dataToCheck = angular.isDefined(item.key) ? d.data[item.key] : d.data; + if (item.check === "is-int") { + pass = angular.isNumber(dataToCheck, item.postfix); + if (!pass) { + error(item.check, {key: item.key}, item.postfix); + } + + } else if (item.check === "is-array") { + pass = angular.isArray(dataToCheck, item.postfix); + if (!pass) { + error(item.check, {key: item.key}, item.postfix); + } + } else if (item.check === "lambda") { + if (!item.validator(dataToCheck)) { + var errorData = {key: item.key}; + if (!angular.isUndefined(item.appendOnErrorLambda)) { + errorData = item.appendOnErrorLambda(dataToCheck); + } + if (_.isObject(item.append) && + _.isString(item.append.key) && + !_.isUndefined(item.append.value)) { + errorData[item.append.key] = evalValue(item.append.value, item); + } + error(item.check, errorData, item.postfix); + } + + } else if (item.check === "is-string-if-defined") { + pass = angular.isUndefined(dataToCheck) || + angular.isString(dataToCheck, item.postfix); + if (!pass) { + error(item.check, {key: item.key}, item.postfix); + } + + } else if (item.check === "array-length-if-defined") { + if (angular.isDefined(dataToCheck)) { + itemMin = evalValue(item.min, d.data); + itemMax = evalValue(item.max, d.data); + + if (angular.isArray(dataToCheck) || angular.isString(dataToCheck)) + { + min = angular.isUndefined(item.min) || dataToCheck.length >= itemMin; + max = angular.isUndefined(item.max) || dataToCheck.length <= itemMax; + pass = min && max; + if (!min) { + error( + "array-length-min", + {key: item.key, min: itemMin, num: dataToCheck.length}, + item.postfix); + } + if (!max) { + var itemErrorData0 = {key: item.key, max: itemMax, num: dataToCheck.length}; + error( + "array-length-max", + itemErrorData0, + item.postfix); + } + } + } + } else if (item.check === "is-string") { + pass = angular.isString(dataToCheck, item.postfix); + if (!pass) { + error(item.check, {key: item.key}, item.postfix); + } + + } else if (item.check === "array-length") { + itemMin = evalValue(item.min, d.data); + itemMax = evalValue(item.max, d.data); + + if (angular.isArray(dataToCheck) || angular.isString(dataToCheck)) + { + min = angular.isUndefined(item.min) || dataToCheck.length >= itemMin; + max = angular.isUndefined(item.max) || dataToCheck.length <= itemMax; + pass = min && max; + if (!min) { + error( + "array-length-min", + {key: item.key, min: itemMin, num: dataToCheck.length}, + item.postfix); + } + if (!max) { + var itemErrorData = {key: item.key, max: itemMax, num: dataToCheck.length}; + error( + "array-length-max", + itemErrorData, + item.postfix); + } + } + + } else if (item.check === "int-size") { + itemMin = evalValue(item.min, d.data); + itemMax = evalValue(item.max, d.data); + min = angular.isUndefined(item.min) || dataToCheck >= itemMin; + max = angular.isUndefined(item.max) || dataToCheck <= itemMax; + pass = min && max; + if (!min) { + error( + "int-size-min", + {key: item.key, min: itemMin, value: dataToCheck}, + item.postfix); + } + if (!max) { + error( + "int-size-max", + {key: item.key, max: itemMax, value: dataToCheck}, + item.postfix); + } + } else if (item.check === "group-chain") { + pass = _.all( + _.map( + item.checks, + function(check) { return checker({ - data: d.data, - errorData: d.errorData, - onError: d.onError, - checks: [ check ], - prefix: sumStrs(d.prefix, item.prefix) + data: d.data, + errorData: d.errorData, + onError: d.onError, + checks: [check], + prefix: sumStrs(d.prefix, item.prefix) }); - })) : "array-key-group-chain" === item.check ? pass = _.every(dataToCheck, function(data, index) { - var extra = {}, prefix = ""; - return angular.isString(d.prefix) && (prefix = d.prefix), angular.isString(item.prefix) && (prefix += item.prefix), - extra.prefix = prefix, extra[item.append.key] = evalValue(item.append.value, data), - checker({ - data: data, - errorData: angular.extend({}, d.errorData, extra), - onError: d.onError, - checks: item.checks, - prefix: sumStrs(d.prefix, item.prefix) - }); - }) : "array-group-chain" === item.check ? pass = _.every(d.data, function(data, index) { - var extra = {}; - return extra[item.append.key] = evalValue(item.append.value, data), checker({ - data: data, - errorData: angular.extend({}, d.errorData, extra), - onError: d.onError, - checks: item.checks, - prefix: sumStrs(d.prefix, item.prefix) - }); - }) : "array-group" === item.check ? pass = _.contains(_.map(d.data, function(data, index) { + }) + ); + } else if (item.check === "array-key-group-chain") { + pass = _.every( + dataToCheck, + function (data, index) { + var extra = {}; + var prefix = ""; + if (angular.isString(d.prefix)) { + prefix = d.prefix; + } + if (angular.isString(item.prefix)) { + prefix += item.prefix; + } + extra.prefix = prefix; + extra[item.append.key] = evalValue(item.append.value, data); + return checker({ + data: data, + errorData: angular.extend({}, d.errorData, extra), + onError: d.onError, + checks: item.checks, + prefix: sumStrs(d.prefix, item.prefix), + }); + }); + } else if (item.check === "array-group-chain") { + pass = _.every(d.data, function (data, index) { + var extra = {}; + extra[item.append.key] = evalValue(item.append.value, data); + return checker({ + data: data, + errorData: angular.extend({}, d.errorData, extra), + onError: d.onError, + checks: item.checks, + prefix: sumStrs(d.prefix, item.prefix), + }); + }); + } else if (item.check === "array-group") { + pass = _.contains( + _.map( + d.data, + function (data, index) { var extra = {}; - return extra[item.append.key] = evalValue(item.append.value, data), checker({ - data: data, - errorData: angular.extend({}, d.errorData, extra), - onError: d.onError, - checks: item.checks, - prefix: sumStrs(d.prefix, item.prefix) + extra[item.append.key] = evalValue(item.append.value, data); + return checker({ + data: data, + errorData: angular.extend({}, d.errorData, extra), + onError: d.onError, + checks: item.checks, + prefix: sumStrs(d.prefix, item.prefix), }); - }), !0) : "object-key-chain" === item.check && (pass = _.isString(item.key) && _.isObject(dataToCheck)) && (data = dataToCheck, - (extra = {})[item.append.key] = evalValue(item.append.value, data), prefix = "", - angular.isString(d.prefix) && (prefix += d.prefix), angular.isString(item.prefix) && (prefix += item.prefix), - pass = _.every(item.checks, function(check, index) { + }), + true); + } else if (item.check === "object-key-chain") { + pass = _.isString(item.key) && _.isObject(dataToCheck); + if (!!pass) { + var data = dataToCheck; + var extra = {}; + extra[item.append.key] = evalValue(item.append.value, data); + var prefix = ""; + if (angular.isString(d.prefix)) { + prefix += d.prefix; + } + if (angular.isString(item.prefix)) { + prefix += item.prefix; + } + pass = _.every( + item.checks, + function (check, index) { return checker({ - data: data, - errorData: angular.extend({}, d.errorData, extra), - onError: d.onError, - checks: [ check ], - prefix: prefix + data: data, + errorData: angular.extend({}, d.errorData, extra), + onError: d.onError, + checks: [check], + prefix: prefix, }); - })), !(!pass && "chain" === d.data.groupType); - }); - return ret; + }); + } + } + if (!pass && d.data.groupType === 'chain') { + return false; + } + return true; + }); + + return ret; } return checker; -}), angular.module("avUi").service("AddDotsToIntService", function() { - return function(number, fixedDigits) { - var number_str = ((number = angular.isNumber(fixedDigits) && 0 <= fixedDigits ? number.toFixed(parseInt(fixedDigits)) : number) + "").replace(".", ","), ret = "", commaPos = number_str.length; - -1 !== number_str.indexOf(",") && (commaPos = number_str.indexOf(",")); - for (var i = 0; i < commaPos; i++) { - var reverse = commaPos - i; - reverse % 3 == 0 && 0 < reverse && 0 < i && (ret += "."), ret += number_str[i]; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * Given a number, adds dots every three digits. + * + * Example: + * + * AddDotsToIntService(1234567) --> "1.234.567" + * AddDotsToIntService(1111.234567) --> "1.111,234567" + */ +angular.module('avUi') + .service('AddDotsToIntService', function() { + return function (number, fixedDigits) { + if (angular.isNumber(fixedDigits) && fixedDigits >= 0) { + number = number.toFixed(parseInt(fixedDigits)); + } + var number_str = (number + "").replace(".", ","); + var ret = ""; + var commaPos = number_str.length; + if (number_str.indexOf(",") !== -1) { + commaPos = number_str.indexOf(","); + } + for (var i = 0; i < commaPos; i++) { + var reverse = commaPos - i; + if ((reverse % 3 === 0) && reverse > 0 && i > 0) { + ret = ret + "."; } - return ret + number_str.substr(commaPos, number_str.length); + ret = ret + number_str[i]; + } + return ret + number_str.substr(commaPos, number_str.length); }; -}), angular.module("avUi").service("EndsWithService", function() { - return function(originString, searchString) { - if (!angular.isString(originString) || !angular.isString(searchString)) return !1; + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avUi') + .service('EndsWithService', function() { + return function (originString, searchString) { + if (!angular.isString(originString) || !angular.isString(searchString)) { + return false; + } var lastIndex = originString.indexOf(searchString); - return -1 !== lastIndex && lastIndex === originString.length - searchString.length; - }; -}), angular.module("avUi").service("StateDataService", [ "$state", function($state) { + return lastIndex !== -1 && lastIndex === originString.length - searchString.length; + }; + }); +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/* + * Save data between states. + * + * Example: + * + * StateDataService.go('election.public.show.login', {id: autheventid}, {something: "foo"}) + * StateDataService.getData() --> {something: "foo"} + */ +angular.module('avUi') + .service('StateDataService', function($state) { var data = {}; return { - go: function(path, stateData, newData) { - data = angular.copy(newData), $state.go(path, stateData); - }, - getData: function() { - return data; - } + go: function (path, stateData, newData) { + data = angular.copy(newData); + $state.go(path, stateData); + }, + getData: function () { + return data; + } }; -} ]), angular.module("avUi").directive("avScrollToBottom", [ "$timeout", function($timeout) { + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/** + * Always scrolls to bottom the div to which the directive is attached when + * the observed property is modified. + * + * Example: + * + *
+ */ +angular.module('avUi') + .directive('avScrollToBottom', function($timeout) { return { - restrict: "A", - link: function(scope, element, attrs) { - scope.$watch(function() { - return element.children().length; - }, function() { - element.animate({ - scrollTop: element.prop("scrollHeight") - }, 300); - }); - } + restrict: 'A', + link: function postLink(scope, element, attrs) { + scope.$watch( + function () { + return element.children().length; + }, + function () { + element.animate({ scrollTop: element.prop('scrollHeight') }, 300); + } + ); + } }; -} ]), angular.module("avUi").filter("addTargetBlank", function() { - return function(tree) { - tree = angular.element("
" + tree + "
"); - return tree.find("a").attr("target", "_blank"), angular.element("
").append(tree).html(); +}); +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +/** + * Adds target blank to links. + * + * Usage example: + * + *
+ */ +angular.module('avUi') + .filter('addTargetBlank', function(){ + return function(x) { + //defensively wrap in a div to avoid 'invalid html' exception, then add + //the target _blank to links + var tree = angular.element('
'+x+'
'); + tree.find('a').attr('target', '_blank'); + + //trick to have a string representation + return angular.element('
').append(tree).html(); }; -}), angular.module("avUi").filter("htmlToText", function() { + }); +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module('avUi') + .filter('htmlToText', function() { return function(text) { - return angular.element("
" + text + "
").text(); + return angular.element('
'+text+'
').text(); }; -}), angular.module("avUi").config([ "$provide", function($provide) { - $provide.decorator("ngModelDirective", [ "$delegate", function($delegate) { - var ngModel = $delegate[0], controller = ngModel.controller; - return ngModel.controller = [ "$scope", "$element", "$attrs", "$injector", function(scope, element, attrs, $injector) { - var $interpolate = $injector.get("$interpolate"); - attrs.$set("name", $interpolate(attrs.name || "")(scope)), $injector.invoke(controller, Object.setPrototypeOf(this, controller.prototype), { - $scope: scope, - $element: element, - $attrs: attrs - }); - } ], $delegate; - } ]), $provide.decorator("formDirective", [ "$delegate", function($delegate) { - var form = $delegate[0], controller = form.controller; - return form.controller = [ "$scope", "$element", "$attrs", "$injector", function(scope, element, attrs, $injector) { - var $interpolate = $injector.get("$interpolate"); - attrs.$set("name", $interpolate(attrs.name || attrs.ngForm || "")(scope)), $injector.invoke(controller, Object.setPrototypeOf(this, controller.prototype), { - $scope: scope, - $element: element, - $attrs: attrs - }); - } ], $delegate; - } ]); -} ]), angular.module("avUi").controller("DocumentationUiController", [ "$state", "$stateParams", "$http", "$scope", "$sce", "$i18next", "ConfigService", "InsideIframeService", "Authmethod", function($state, $stateParams, $http, $scope, $sce, $i18next, ConfigService, InsideIframeService, Authmethod) { - $scope.inside_iframe = InsideIframeService(), $scope.documentation = ConfigService.documentation, - $scope.documentation.security_contact = ConfigService.legal.security_contact, $scope.documentation_html_include = $sce.trustAsHtml(ConfigService.documentation_html_include), - $scope.auths_url = "/election/" + $stateParams.id + "/public/authorities", $scope.election_id = $stateParams.id + "", - Authmethod.viewEvent($stateParams.id).then(function(response) { - "ok" === response.data.status && ($scope.authEvent = response.data.events); + }); +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +// see https://github.com/angular/angular.js/issues/1404 +angular.module('avUi') + .config(function($provide) { + $provide.decorator('ngModelDirective', function($delegate) { + var ngModel = $delegate[0], controller = ngModel.controller; + ngModel.controller = ['$scope', '$element', '$attrs', '$injector', function(scope, element, attrs, $injector) { + var $interpolate = $injector.get('$interpolate'); + attrs.$set('name', $interpolate(attrs.name || '')(scope)); + $injector.invoke(controller, Object.setPrototypeOf(this, controller.prototype), { + '$scope': scope, + '$element': element, + '$attrs': attrs + }); + }]; + return $delegate; + }); + $provide.decorator('formDirective', function($delegate) { + var form = $delegate[0], controller = form.controller; + form.controller = ['$scope', '$element', '$attrs', '$injector', function(scope, element, attrs, $injector) { + var $interpolate = $injector.get('$interpolate'); + attrs.$set('name', $interpolate(attrs.name || attrs.ngForm || '')(scope)); + $injector.invoke(controller, Object.setPrototypeOf(this, controller.prototype), { + '$scope': scope, + '$element': element, + '$attrs': attrs + }); + }]; + return $delegate; }); -} ]), angular.module("avUi").directive("documentationDirective", function() { + }); + + /** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-elections is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-elections is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-elections. If not, see . +**/ + +/* + * Shows the public view of an election. Controls mainly the changing inner states + * loading config, showing results, showing error if needed. + */ +angular.module('avUi').controller('DocumentationUiController', + function($state, $stateParams, $http, $scope, $sce, $i18next, ConfigService, InsideIframeService, Authmethod) { + $scope.inside_iframe = InsideIframeService(); + $scope.documentation = ConfigService.documentation; + $scope.documentation.security_contact = ConfigService.legal.security_contact; + $scope.documentation_html_include = $sce.trustAsHtml(ConfigService.documentation_html_include); + $scope.auths_url = '/election/' + $stateParams.id + '/public/authorities'; + $scope.election_id = $stateParams.id + ''; + + Authmethod.viewEvent($stateParams.id) + .then(function(response) { + if (response.data.status === "ok") { + $scope.authEvent = response.data.events; + } + }); + } +); + +angular.module('avUi') + .directive('documentationDirective', function() { return { - restrict: "AE", - scope: { - extra: "=" - }, - templateUrl: "avUi/documentation-directive/documentation-directive.html", - controller: "DocumentationUiController" + restrict: 'AE', + scope: { + extra: '=' + }, + templateUrl: 'avUi/documentation-directive/documentation-directive.html', + controller: 'DocumentationUiController' }; -}), angular.module("avUi").directive("avFoot", [ "ConfigService", function(ConfigService) { + }); + +/** + * This file is part of agora-gui-admin. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-admin is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-admin is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-admin. If not, see . +**/ + +angular.module('avUi') + .directive('avFoot', function(ConfigService) { + // we use it as something similar to a controller here + function link(scope, element, attrs) { + scope.contact = ConfigService.contact; + scope.social = ConfigService.social; + scope.technology = ConfigService.technology; + scope.legal = ConfigService.legal; + scope.organization = ConfigService.organization; + } + return { - restrict: "AE", - scope: {}, - link: function(scope, element, attrs) { - scope.contact = ConfigService.contact, scope.social = ConfigService.social, scope.technology = ConfigService.technology, - scope.legal = ConfigService.legal, scope.organization = ConfigService.organization; - }, - templateUrl: "avUi/foot-directive/foot-directive.html" + restrict: 'AE', + scope: { + }, + link: link, + templateUrl: 'avUi/foot-directive/foot-directive.html' }; -} ]), angular.module("agora-gui-common", [ "ui.bootstrap", "ui.utils", "ui.router", "ngAnimate", "ngResource", "ngCookies", "ipCookie", "ngSanitize", "infinite-scroll", "angularMoment", "avConfig", "jm.i18next", "avRegistration", "avUi", "avTest", "angularFileUpload", "dndLists", "angularLoad", "ng-autofocus" ]), -angular.module("jm.i18next").config([ "$i18nextProvider", "ConfigServiceProvider", function($i18nextProvider, ConfigServiceProvider) { - $("#no-js").hide(), $i18nextProvider.options = _.extend({ - useCookie: !0, - useLocalStorage: !1, - fallbackLng: "en", - cookieName: "lang", - detectLngQS: "lang", - lngWhitelist: [ "en", "es", "gl", "ca" ], - resGetPath: "/locales/__lng__.json", - defaultLoadingValue: "" - }, ConfigServiceProvider.i18nextInitOptions); -} ]), angular.module("agora-gui-common").run([ "$http", "$rootScope", function($http, $rootScope) { - $rootScope.safeApply = function(fn) { - var phase = $rootScope.$$phase; - "$apply" === phase || "$digest" === phase ? fn && "function" == typeof fn && fn() : this.$apply(fn); - }, $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) { - console.log("change start from " + fromState.name + " to " + toState.name), $("#angular-preloading").show(); - }), $rootScope.$on("$stateChangeSuccess", function(event, toState, toParams, fromState, fromParams) { - console.log("change success"), $("#angular-preloading").hide(); + }); + +/** + * This file is part of agora-gui-common. + * Copyright (C) 2015-2016 Agora Voting SL + + * agora-gui-common is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License. + + * agora-gui-common is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + + * You should have received a copy of the GNU Affero General Public License + * along with agora-gui-common. If not, see . +**/ + +angular.module( + 'agora-gui-common', + ['ui.bootstrap', + 'ui.utils', + 'ui.router', + 'ngAnimate', + 'ngResource', + 'ngCookies', + 'ipCookie', + 'ngSanitize', + 'infinite-scroll', + 'angularMoment', + 'avConfig', + 'jm.i18next', + 'avRegistration', + 'avUi', + 'avTest', + 'angularFileUpload', + 'dndLists', + 'angularLoad', + 'ng-autofocus' +]); + +angular.module('jm.i18next').config(function ($i18nextProvider, ConfigServiceProvider) { + // note that we do not send the language: by default, it will try the language + // supported by the web browser + $("#no-js").hide(); + + $i18nextProvider.options = _.extend( + { + useCookie: true, + useLocalStorage: false, + fallbackLng: 'en', + cookieName: 'lang', + detectLngQS: 'lang', + lngWhitelist: ['en', 'es', 'gl', 'ca'], + resGetPath: '/locales/__lng__.json', + defaultLoadingValue: '' // ng-i18next option, *NOT* directly supported by i18next + }, + ConfigServiceProvider.i18nextInitOptions); +}); + +angular.module('agora-gui-common').run(function($http, $rootScope) { + + $rootScope.safeApply = function(fn) { + var phase = $rootScope.$$phase; + if (phase === '$apply' || phase === '$digest') { + if (fn && (typeof(fn) === 'function')) { + fn(); + } + } else { + this.$apply(fn); + } + }; + + $rootScope.$on('$stateChangeStart', + function(event, toState, toParams, fromState, fromParams) { + console.log("change start from " + fromState.name + " to " + toState.name); + $("#angular-preloading").show(); + }); + $rootScope.$on('$stateChangeSuccess', + function(event, toState, toParams, fromState, fromParams) { + console.log("change success"); + $("#angular-preloading").hide(); + }); +}); + +/* +This directive will trigger a click if the user presses space or enter + */ +angular.module('agora-gui-common').directive('ngSpaceClick', function ($timeout) { + return function (scope, element, attrs) { + element.bind("keydown", function (event) { + switch (event.which) { + case 13: // ENTER + case 32: { // SPACE + $timeout(function() {event.currentTarget.click();},0); + event.stopPropagation(); + } + } }); -} ]), angular.module("agora-gui-common").directive("ngSpaceClick", [ "$timeout", function($timeout) { - return function(scope, element, attrs) { - element.bind("keydown", function(event) { - switch (event.which) { - case 13: - case 32: - $timeout(function() { - event.currentTarget.click(); - }, 0), event.stopPropagation(); + }; +}); + +/* +This directive allows us to pass a function in on an enter key to do what we want. + */ +angular.module('agora-gui-common').directive('ngEnter', function () { + return function (scope, element, attrs) { + element.bind("keydown keypress", function (event) { + if(event.which === 13) { + scope.$apply(function (){ + scope.$eval(attrs.ngEnter); + }); + + event.preventDefault(); } }); }; -} ]), angular.module("agora-gui-common").directive("ngEnter", function() { - return function(scope, element, attrs) { - element.bind("keydown keypress", function(event) { - 13 === event.which && (scope.$apply(function() { - scope.$eval(attrs.ngEnter); - }), event.preventDefault()); - }); - }; -}), angular.module("agora-gui-common").filter("truncate", function() { - return function(text, length, end) { - return isNaN(length) && (length = 10), void 0 === end && (end = "..."), text.length <= length || text.length - end.length <= length ? text : String(text).substring(0, length - end.length) + end; - }; -}), avConfigData.browserUpdate) try { - document.addEventListener("DOMContentLoaded", $buo_f, !1); -} catch (e) { +}); + +/** + * Truncate Filter + * @Param text + * @Param length, default is 10 + * @Param end, default is "..." + * @return string + */ +angular.module('agora-gui-common').filter('truncate', function () { + return function (text, length, end) { + if (isNaN(length)) { + length = 10; + } + + if (end === undefined) { + end = "..."; + } + + if (text.length <= length || text.length - end.length <= length) { + return text; + } + else { + return String(text).substring(0, length-end.length) + end; + } + + }; + }); + +/*globals avConfigData:false, $buo:false */ +/** + * Check browser version with browser-update.org + */ +function $buo_f() { + $buo(avConfigData.browserUpdate); +} + +if (avConfigData.browserUpdate) { + try { + document.addEventListener("DOMContentLoaded", $buo_f, false); + } catch (e) { window.attachEvent("onload", $buo_f); + } } -angular.module("avTest", []), angular.module("avTest").controller("UnitTestE2EController", [ "$scope", "$location", "ConfigService", function($scope, $location, ConfigService) { - ConfigService.debug && ($scope.html = $location.search().html, console.log($location.search())); -} ]), angular.module("agora-gui-common").run([ "$templateCache", function($templateCache) { - "use strict"; - $templateCache.put("avRegistration/error.html", '

'), - $templateCache.put("avRegistration/field-directive/field-directive.html", '
'), - $templateCache.put("avRegistration/fields/bool-field-directive/bool-field-directive.html", '

'), - $templateCache.put("avRegistration/fields/captcha-field-directive/captcha-field-directive.html", '

{{field.help}}

{{authMethod.captcha_status}}
'), - $templateCache.put("avRegistration/fields/code-field-directive/code-field-directive.html", '

'), - $templateCache.put("avRegistration/fields/date-field-directive/date-field-directive.html", '

{{field.help}}

'), - $templateCache.put("avRegistration/fields/dni-field-directive/dni-field-directive.html", '

'), - $templateCache.put("avRegistration/fields/email-field-directive/email-field-directive.html", '

'), - $templateCache.put("avRegistration/fields/image-field-directive/image-field-directive.html", '

'), - $templateCache.put("avRegistration/fields/int-field-directive/int-field-directive.html", '

{{field.help}}

'), - $templateCache.put("avRegistration/fields/password-field-directive/password-field-directive.html", '

'), - $templateCache.put("avRegistration/fields/tel-field-directive/tel-field-directive.html", '

'), - $templateCache.put("avRegistration/fields/text-field-directive/text-field-directive.html", '

{{field.help}}

'), - $templateCache.put("avRegistration/fields/textarea-field-directive/textarea-field-directive.html", '

{{field.help}}

'), - $templateCache.put("avRegistration/loading.html", '

'), - $templateCache.put("avRegistration/login-controller/login-controller.html", '
'), - $templateCache.put("avRegistration/login-directive/login-directive.html", '

{{ error }}
avRegistration.fillValidFormText
'), - $templateCache.put("avRegistration/openid-connect-directive/openid-connect-directive.html", ""), - $templateCache.put("avRegistration/register-controller/register-controller.html", '
'), - $templateCache.put("avRegistration/register-directive/register-directive.html", '

avRegistration.fillValidFormText
'), - $templateCache.put("avRegistration/success.html", '

'), - $templateCache.put("avUi/change-lang-directive/change-lang-directive.html", ''), - $templateCache.put("avUi/children-elections-directive/children-elections-directive.html", '
avAdmin.childrenElections.main

{{category.title}}

{{election.title}}
'), - $templateCache.put("avUi/documentation-directive/documentation-directive.html", '

'), - $templateCache.put("avUi/foot-directive/foot-directive.html", '
'), - $templateCache.put("avUi/simple-error-directive/simple-error-directive.html", '
'), - $templateCache.put("test/test_booth_widget.html", 'Test frame
Votar con Agora Voting
" + ); + + + $templateCache.put('test/unit_test_e2e.html', + "
" + ); + +}]);