Commit f18db24b9c4c75d874d05e6d23ab36da5758b395

Authored by Leonardo Merlin
1 parent d9e6f9c7
Exists in feature-captcha

Add reCaptcha (issue #56)

bower.json
... ... @@ -7,6 +7,7 @@
7 7 "angular-breadcrumb": "~0.4.1",
8 8 "angular-cookies": "~1.4.0",
9 9 "angular-messages": "ng-messages#*",
  10 + "angular-recaptcha": "~2.2.5",
10 11 "angular-sanitize": "~1.4.0",
11 12 "angular-slugify": "~1.0.1",
12 13 "angular-socialshare": "angularjs-socialshare#~0.1.13",
... ...
src/app/components/proposal-box/proposal-box.directive.js
... ... @@ -9,21 +9,23 @@
9 9 function proposalBox() {
10 10  
11 11 /** @ngInject */
12   - function ProposalBoxController($scope, $location, $rootScope, $state, $timeout, $interval, $window, VOTE_STATUS, VOTE_OPTIONS, AuthService, DialogaService, $log) {
  12 + function ProposalBoxController($scope, $location, $rootScope, $state, $timeout, $interval, $window, APP, VOTE_STATUS, VOTE_OPTIONS, AuthService, DialogaService, vcRecaptchaService, $log) {
13 13 $log.debug('ProposalBoxController');
14 14  
15 15 var vm = this;
16 16 vm.$scope = $scope;
  17 + vm.$location = $location;
17 18 vm.$rootScope = $rootScope;
18 19 vm.$state = $state;
19 20 vm.$timeout = $timeout;
20 21 vm.$interval = $interval;
21 22 vm.$window = $window;
  23 + vm.APP = APP;
22 24 vm.VOTE_STATUS = VOTE_STATUS;
23 25 vm.VOTE_OPTIONS = VOTE_OPTIONS;
24 26 vm.AuthService = AuthService;
  27 + vm.vcRecaptchaService = vcRecaptchaService;
25 28 vm.$log = $log;
26   - vm.$location = $location;
27 29  
28 30 vm.init();
29 31 vm.addListeners();
... ... @@ -39,6 +41,8 @@
39 41 vm.STATE = null;
40 42 vm.errorOnSkip = false;
41 43 vm.showCaptchaForm = null;
  44 + vm.recaptchaWidgetId = null;
  45 + vm.recaptchaResponse = null;
42 46 vm.voteProposalRedirectURI = null;
43 47 vm.proposalsImg = null;
44 48  
... ... @@ -86,20 +90,27 @@
86 90 vm.messageCode = data.code;
87 91 });
88 92  
89   - // Load captcha
90   - var stop = null;
91   - stop = vm.$interval(function() {
92   - var $el = angular.element('#serpro_captcha');
93   -
94   - if ($el && $el.length > 0) {
95   - vm.$window.initCaptcha($el[0]);
96   - vm.$interval.cancel(stop);
97   - stop = undefined;
98   - }else {
99   - vm.$log.debug('captcha element not found.');
100   - }
101   -
102   - }, 10);
  93 + // reCaptcha Listeners
  94 + vm.setWidgetId = function(widgetId) {
  95 + // store the `widgetId` for future usage.
  96 + // For example for getting the response with
  97 + // `recaptcha.getResponse(widgetId)`.
  98 + vm.$log.info('Created widget ID:', widgetId);
  99 + vm.recaptchaWidgetId = widgetId;
  100 +
  101 + };
  102 +
  103 + vm.setResponse = function(response) {
  104 +
  105 + // Update local captcha response
  106 + vm.$log.debug('Response available', response);
  107 + vm.recaptchaResponse = response;
  108 + };
  109 +
  110 + vm.cbExpiration = function() {
  111 + // reset the 'response' object that is on scope
  112 + vm.$log.debug('cbExpiration');
  113 + };
103 114 };
104 115  
105 116 ProposalBoxController.prototype.canVote = function() {
... ... @@ -113,12 +124,10 @@
113 124  
114 125 var target = $event.target;
115 126 var $target = angular.element(target);
116   - var $captcha = $target.find('[name="txtToken_captcha_serpro_gov_br"]');
117 127  
118 128 vm.sendingCaptcha = true;
119 129 vm.AuthService.loginCaptcha({
120   - captcha_text: captchaForm.captcha_text.$modelValue,
121   - txtToken_captcha_serpro_gov_br: $captcha.val()
  130 + recaptcha_response: vm.recaptchaResponse
122 131 }).then(function(data) {
123 132 // SUCCESS
124 133 vm.$log.debug('register success.data', data);
... ... @@ -131,22 +140,23 @@
131 140 // hide captcha form
132 141 vm.showCaptchaForm = false;
133 142  
134   - }, function(data) {
  143 + })
  144 + .catch(function(data) {
135 145 // ERROR
136 146 vm.$log.debug('register error.data', data);
137 147  
138   - vm.sendingCaptchaError = {
139   - code: data.status,
140   - message: data.message || ('Erro (' + data.status + '). Já estamos trabalhando para resolver o problema.<br/>Por favor, tente novamente mais tarde')
141   - };
  148 + // In case of a failed validation you need to reload the captcha
  149 + // because each response can be checked just once
  150 + vm.vcRecaptchaService.reload(vm.recaptchaWidgetId);
  151 +
  152 + vm.sendingCaptchaError = {};
  153 + vm.sendingCaptchaError.code = data.status;
  154 + vm.sendingCaptchaError.message = data.message || ('Erro (' + data.status + '). Já estamos trabalhando para resolver o problema.<br/>Por favor, tente novamente mais tarde');
142 155  
143 156 if (angular.equals(vm.sendingCaptchaError.message, 'Internal captcha validation error')) {
144 157 vm.sendingCaptchaError.message = 'Erro interno ao tentar validar captcha.<br/><br/>Já estamos trabalhando para resolver o problema.<br/>Por favor, tente novamente mais tarde.';
145 158 }
146 159  
147   - }, function(data) {
148   - // UPDATE
149   - vm.$log.debug('register update.data', data);
150 160 }).finally(function() {
151 161 vm.sendingCaptcha = false;
152 162 });
... ... @@ -161,11 +171,10 @@
161 171 vm.message = null;
162 172  
163 173 // reload new captcha
164   - var $el = angular.element('#serpro_captcha');
165   - vm.$window.reloadCaptcha($el[0]);
  174 + vm.vcRecaptchaService.reload(vm.recaptchaWidgetId);
166 175  
167 176 // focus on input
168   - angular.element('#captcha_text').val('').focus();
  177 + // angular.element('#captcha_text').val('').focus();
169 178 };
170 179  
171 180 ProposalBoxController.prototype.vote = function(value) {
... ...
src/app/components/proposal-box/proposal-box.html
... ... @@ -93,12 +93,14 @@
93 93 <div ng-hide="vm.sendingCaptchaError">
94 94 <form name="captchaForm" ng-submit="vm.submitCaptcha($event, captchaForm)">
95 95 <div class="form-group">
96   - <div id="serpro_captcha" class="captcha"></div>
97   - <div class="captcha">Digite os caracteres acima:</div>
98   - <div class="captcha">
99   - <input type="text" name="captcha_text" id="captcha_text" aria-label="Escreva os caracteres do captcha aqui" ng-model="vm._captcha_text" ng-minlength="" ng-maxlength="" required>
100   - <validation-messages field="captchaForm.captcha_text"></validation-messages>
101   - </div>
  96 + <div
  97 + vc-recaptcha
  98 + theme="'light'"
  99 + key="vm.APP.recaptcha_key"
  100 + on-create="vm.setWidgetId(widgetId)"
  101 + on-success="vm.setResponse(response)"
  102 + on-expire="vm.cbExpiration()"
  103 + ></div>
102 104 </div>
103 105 <div class="form-group">
104 106 <button type="submit" class="btn btn-lg btn-block btn-submit">Enviar</button>
... ...
src/app/index.constants.js
... ... @@ -11,6 +11,7 @@
11 11 .constant('APP', {
12 12 facebook_app_id: '1',
13 13 google_app_id: '4',
  14 + recaptcha_key: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI',
14 15 })
15 16 .constant('API', {
16 17 token: null,
... ...
src/app/index.module.js
... ... @@ -2,6 +2,6 @@
2 2 'use strict';
3 3  
4 4 angular
5   - .module('dialoga', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ui.router', 'ngStorage', '720kb.socialshare', 'slugifier', 'ncy-angular-breadcrumb', 'ngMessages']);
  5 + .module('dialoga', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ui.router', 'ngStorage', '720kb.socialshare', 'slugifier', 'ncy-angular-breadcrumb', 'ngMessages', 'vcRecaptcha']);
6 6  
7 7 })();
... ...
src/app/pages/auth/auth.controller.js
... ... @@ -6,7 +6,7 @@
6 6 .controller('AuthPageController', AuthPageController);
7 7  
8 8 /** @ngInject */
9   - function AuthPageController($scope, $rootScope, $window, $location, $state, $timeout, $interval, APP, AUTH_EVENTS, AuthService, DialogaService, Session, $log) {
  9 + function AuthPageController($scope, $rootScope, $window, $location, $state, $timeout, $interval, APP, AUTH_EVENTS, AuthService, DialogaService, Session, vcRecaptchaService, $log) {
10 10 var vm = this;
11 11  
12 12 vm.$scope = $scope;
... ... @@ -21,6 +21,7 @@
21 21 vm.AuthService = AuthService;
22 22 vm.DialogaService = DialogaService;
23 23 vm.Session = Session;
  24 + vm.vcRecaptchaService = vcRecaptchaService;
24 25 vm.$log = $log;
25 26  
26 27 vm.init();
... ... @@ -42,6 +43,8 @@
42 43 vm.loadingTerms = null;
43 44 vm.delay = 3; // segundos
44 45 vm.countdown = 0;
  46 + vm.recaptchaResponse = null;
  47 + vm.recaptchaWidgetId = null;
45 48  
46 49 vm.search = vm.$location.search();
47 50 var redirect = vm.search.redirect_uri || '';
... ... @@ -62,7 +65,6 @@
62 65 vm.$scope.$on(vm.AUTH_EVENTS.logoutSuccess, function() {
63 66 vm.clearMessages();
64 67 vm.currentUser = vm.Session.getCurrentUser();
65   - vm._attachCaptcha();
66 68 });
67 69 };
68 70  
... ... @@ -104,7 +106,7 @@
104 106 var private_token = response.data.private_token;
105 107  
106 108 // Garante que o 'user' sempre terá a propriedade 'private_token'
107   - if(response.data.user && !response.data.user.private_token){
  109 + if (response.data.user && !response.data.user.private_token) {
108 110 response.data.user.private_token = private_token;
109 111 }
110 112  
... ... @@ -117,23 +119,27 @@
117 119 }
118 120 });
119 121  
120   - vm._attachCaptcha();
121   - };
122   -
123   - AuthPageController.prototype._attachCaptcha = function() {
124   - var vm = this;
125   -
126   - var stop = null;
127   - stop = vm.$interval(function() {
128   - var $el = angular.element('#serpro_captcha');
  122 + // reCaptcha Listeners
  123 + vm.setWidgetId = function(widgetId) {
  124 + // store the `widgetId` for future usage.
  125 + // For example for getting the response with
  126 + // `recaptcha.getResponse(widgetId)`.
  127 + vm.$log.info('Created widget ID:', widgetId);
  128 + vm.recaptchaWidgetId = widgetId;
  129 +
  130 + };
129 131  
130   - if ($el && $el.length > 0) {
131   - vm.$window.initCaptcha($el[0]);
132   - vm.$interval.cancel(stop);
133   - stop = undefined;
134   - }
  132 + vm.setResponse = function(response) {
  133 +
  134 + // Update local captcha response
  135 + vm.$log.debug('Response available', response);
  136 + vm.recaptchaResponse = response;
  137 + };
135 138  
136   - }, 200);
  139 + vm.cbExpiration = function() {
  140 + // reset the 'response' object that is on scope
  141 + vm.$log.debug('cbExpiration');
  142 + };
137 143 };
138 144  
139 145 AuthPageController.prototype.onClickLogout = function() {
... ... @@ -145,8 +151,8 @@
145 151 AuthPageController.prototype.submitSignup = function($event, credentials) {
146 152 var vm = this;
147 153  
148   - credentials.txtToken_captcha_serpro_gov_br = getCaptchaValFromEvent($event);
149   -
  154 + credentials.recaptcha_response = vm.recaptchaResponse;
  155 +
150 156 vm.AuthService.register(credentials)
151 157 .then(function(/*response*/) {
152 158 // SUCCESS
... ... @@ -154,14 +160,12 @@
154 160 vm.signupSuccess = true;
155 161 // vm._startRedirect();
156 162  
157   - }, function(response) {
158   - // ERROR
159   -
160   - // TODO: mensagens de erro
161   - // TODO: tratar multiplos erros
162   -
163   - // javascript_console_message: "Unable to reach Serpro's Captcha validation service"
164   - // message: "Internal captcha validation error"
  163 + })
  164 + .catch(function(response) {
  165 +
  166 + // In case of a failed validation you need to reload the captcha
  167 + // because each response can be checked just once
  168 + vm.vcRecaptchaService.reload(vm.recaptchaWidgetId);
165 169  
166 170 vm.signupError = true;
167 171 vm.signupErrorTitle = 'Erro!';
... ... @@ -179,6 +183,9 @@
179 183 if (response.status >= 500 && response.status < 600) {
180 184 vm.internalError = true;
181 185 }
  186 + })
  187 + .finally(function(){
  188 + // vm.loadingSubmit = false;
182 189 });
183 190 };
184 191  
... ... @@ -220,8 +227,7 @@
220 227 // get form data
221 228 var data = {
222 229 login: recoverForm.login.$modelValue,
223   - captcha_text: recoverForm.captcha_text.$modelValue,
224   - txtToken_captcha_serpro_gov_br: getCaptchaValFromEvent($event)
  230 + recaptcha_response: vm.recaptchaResponse
225 231 };
226 232  
227 233 var promiseRequest = vm.AuthService.forgotPassword(data);
... ... @@ -265,12 +271,10 @@
265 271 // get form data
266 272 var data = {
267 273 login: confirmationForm.login.$modelValue,
268   - captcha_text: confirmationForm.captcha_text.$modelValue,
269   - txtToken_captcha_serpro_gov_br: getCaptchaValFromEvent($event)
  274 + recaptcha_response: vm.recaptchaResponse
270 275 };
271 276  
272 277 // get captcha token
273   -
274 278 vm.AuthService.resendConfirmation(data)
275 279 .then(function(response) {
276 280 vm.$log.debug('resendConfirmation success.response', response);
... ... @@ -287,7 +291,8 @@
287 291 vm.resendConfirmationSuccessMessage = 'Em instantes você receberá em seu e-mail um link para confirmar o seu cadastro.';
288 292 }
289 293  
290   - }, function(response) {
  294 + })
  295 + .catch(function(response) {
291 296 vm.$log.debug('resendConfirmation error.response', response);
292 297  
293 298 vm.resendConfirmationError = true;
... ... @@ -300,8 +305,9 @@
300 305 if (response.status >= 500 && response.status < 600) {
301 306 vm.internalError = true;
302 307 }
303   - }).catch(function(error) {
304   - vm.$log.debug('resendConfirmation catch.error', error);
  308 + })
  309 + .finally(function() {
  310 +
305 311 });
306 312 };
307 313  
... ... @@ -382,8 +388,4 @@
382 388 var url = 'http://login.dialoga.gov.br/plugin/oauth_client/google_oauth2?oauth_client_popup=true&id=' + vm.APP.google_app_id;
383 389 vm.$window.oauthClientAction(url);
384 390 };
385   -
386   - function getCaptchaValFromEvent($event) {
387   - return angular.element($event.target).find('[name="txtToken_captcha_serpro_gov_br"]').val();
388   - }
389 391 })();
... ...
src/app/pages/auth/recover.html
... ... @@ -48,15 +48,14 @@
48 48 <validation-messages field=" recoverPassForm.login"></validation-messages>
49 49 </div>
50 50 <div class="form-group">
51   - <div id="serpro_captcha" class="captcha">
52   - </div>
53   - <div class="captcha">
54   - Digite os caracteres acima:
55   - </div>
56   - <div class="captcha">
57   - <input type="text" name="captcha_text" id="captcha_text" aria-label="Escreva os caracteres do captcha aqui" ng-model="pageAuth.signup.captcha_text" ng-minlength="" ng-maxlength="" required>
58   - <validation-messages field="recoverPassForm.captcha_text"></validation-messages>
59   - </div>
  51 + <div
  52 + vc-recaptcha
  53 + theme="'light'"
  54 + key="pageAuth.APP.recaptcha_key"
  55 + on-create="pageAuth.setWidgetId(widgetId)"
  56 + on-success="pageAuth.setResponse(response)"
  57 + on-expire="pageAuth.cbExpiration()"
  58 + ></div>
60 59 </div>
61 60 <div class="form-group">
62 61 <button class="btn btn-lg btn-submit" type="submit">Solicitar alteração de senha</button>
... ...
src/app/pages/auth/resend-confirmation.html
... ... @@ -60,14 +60,14 @@
60 60 <validation-messages field=" confirmationForm.login"></validation-messages>
61 61 </div>
62 62 <div class="form-group">
63   - <div id="serpro_captcha" class="captcha"></div>
64   - <div class="captcha">
65   - Digite os caracteres acima:
66   - </div>
67   - <div class="captcha">
68   - <input type="text" name="captcha_text" id="captcha_text" aria-label="Escreva os caracteres do captcha aqui" ng-model="pageAuth.signup.captcha_text" ng-minlength="" ng-maxlength="" required>
69   - <validation-messages field="confirmationForm.captcha_text"></validation-messages>
70   - </div>
  63 + <div
  64 + vc-recaptcha
  65 + theme="'light'"
  66 + key="pageAuth.APP.recaptcha_key"
  67 + on-create="pageAuth.setWidgetId(widgetId)"
  68 + on-success="pageAuth.setResponse(response)"
  69 + on-expire="pageAuth.cbExpiration()"
  70 + ></div>
71 71 </div>
72 72 <div class="form-group">
73 73 <button class="btn btn-lg btn-submit" type="submit">Solicitar novo e-mail de confirmação</button>
... ...
src/app/pages/auth/signin.html
... ... @@ -247,15 +247,14 @@
247 247 <validation-messages field="signupForm.user_terms_accepted"></validation-messages>
248 248 </div>
249 249 <div class="form-group">
250   - <div id="serpro_captcha" class="captcha">
251   - </div>
252   - <div class="captcha">
253   - Digite os caracteres acima:
254   - </div>
255   - <div class="captcha">
256   - <input type="text" name="captcha_text" id="captcha_text" aria-label="Escreva os caracteres do captcha aqui" ng-model="pageAuth.signup.captcha_text" ng-minlength="" ng-maxlength="" required>
257   - <validation-messages field="signupForm.captcha_text"></validation-messages>
258   - </div>
  250 + <div
  251 + vc-recaptcha
  252 + theme="'light'"
  253 + key="pageAuth.APP.recaptcha_key"
  254 + on-create="pageAuth.setWidgetId(widgetId)"
  255 + on-success="pageAuth.setResponse(response)"
  256 + on-expire="pageAuth.cbExpiration()"
  257 + ></div>
259 258 </div>
260 259 <div class="form-group">
261 260 <button type="submit" class="btn btn-lg btn-block btn-submit" ng-class=" {'disabled' : !pageAuth.signup.user_terms_accepted }">Cadastrar</button>
... ...
src/app/pages/duvidas/duvidas.controller.js
... ... @@ -6,14 +6,16 @@
6 6 .controller('DuvidasPageController', DuvidasPageController);
7 7  
8 8 /** @ngInject */
9   - function DuvidasPageController(DialogaService, $interval, $window, $log) {
  9 + function DuvidasPageController(DialogaService, APP, $interval, $window, vcRecaptchaService, $log) {
10 10 $log.debug('DuvidasPageController');
11 11  
12 12 var vm = this;
13 13  
14 14 vm.DialogaService = DialogaService;
  15 + vm.APP = APP;
15 16 vm.$interval = $interval;
16 17 vm.$window = $window;
  18 + vm.vcRecaptchaService = vcRecaptchaService;
17 19 vm.$log = $log;
18 20  
19 21 vm.init();
... ... @@ -28,6 +30,8 @@
28 30 vm.error = false;
29 31 vm.sendingContactForm = false;
30 32 vm.questions = [];
  33 + vm.recaptchaResponse = null;
  34 + vm.recaptchaWidgetId = null;
31 35  
32 36 };
33 37  
... ... @@ -51,23 +55,27 @@
51 55 DuvidasPageController.prototype.attachListeners = function () {
52 56 var vm = this;
53 57  
54   - vm._attachCaptcha();
55   - };
56   -
57   - DuvidasPageController.prototype._attachCaptcha = function() {
58   - var vm = this;
59   -
60   - var stop = null;
61   - stop = vm.$interval(function(){
62   - var $el = angular.element('#serpro_captcha');
  58 + // reCaptcha Listeners
  59 + vm.setWidgetId = function(widgetId) {
  60 + // store the `widgetId` for future usage.
  61 + // For example for getting the response with
  62 + // `recaptcha.getResponse(widgetId)`.
  63 + vm.$log.info('Created widget ID:', widgetId);
  64 + vm.recaptchaWidgetId = widgetId;
  65 +
  66 + };
63 67  
64   - if ($el && $el.length > 0 ){
65   - vm.$window.initCaptcha($el[0]);
66   - vm.$interval.cancel(stop);
67   - stop = undefined;
68   - }
  68 + vm.setResponse = function(response) {
  69 +
  70 + // Update local captcha response
  71 + vm.$log.debug('Response available', response);
  72 + vm.recaptchaResponse = response;
  73 + };
69 74  
70   - }, 200);
  75 + vm.cbExpiration = function() {
  76 + // reset the 'response' object that is on scope
  77 + vm.$log.debug('cbExpiration');
  78 + };
71 79 };
72 80  
73 81 DuvidasPageController.prototype.submitContactForm = function ($event, contactForm) {
... ... @@ -82,11 +90,8 @@
82 90 subject: contactForm.inputSubject.$modelValue,
83 91 message: contactForm.inputMessage.$modelValue
84 92 };
85   -
86   - var target = $event.target;
87   - var $target = angular.element(target);
88   - var $captcha = $target.find('[name="txtToken_captcha_serpro_gov_br"]');
89   - data.txtToken_captcha_serpro_gov_br = $captcha.val();
  93 +
  94 + data.recaptcha_response = vm.recaptchaResponse;
90 95  
91 96 vm.DialogaService.sendContactForm(data)
92 97 .then(function(response){
... ...
src/app/pages/duvidas/duvidas.html
... ... @@ -89,12 +89,14 @@
89 89 <div class="col-sm-4 form-group pull-right">
90 90  
91 91 <div class="form-group">
92   - <div id="serpro_captcha" class="captcha"></div>
93   - <div class="captcha">Digite os caracteres acima:</div>
94   - <div class="captcha">
95   - <input type="text" name="captcha_text" id="captcha_text" aria-label="Escreva os caracteres do captcha aqui" ng-model="pageSignin.signup.captcha_text" ng-minlength="" ng-maxlength="" required>
96   - <validation-messages field="contactForm.captcha_text"></validation-messages>
97   - </div>
  92 + <div
  93 + vc-recaptcha
  94 + theme="'light'"
  95 + key="pageDuvidas.APP.recaptcha_key"
  96 + on-create="pageDuvidas.setWidgetId(widgetId)"
  97 + on-success="pageDuvidas.setResponse(response)"
  98 + on-expire="pageDuvidas.cbExpiration()"
  99 + ></div>
98 100 </div>
99 101  
100 102 <div class="row" ng-show="pageDuvidas.sendingContactForm">
... ...
src/index.html
... ... @@ -25,6 +25,9 @@
25 25 <!-- css files will be automatically insert here -->
26 26 <!-- endinject -->
27 27 <!-- endbuild -->
  28 +
  29 + <!-- Google reCaptcha -->
  30 + <script src="https://www.google.com/recaptcha/api.js?onload=vcRecaptchaApiLoaded&render=explicit" async defer></script>
28 31 </head>
29 32 <body ng-cloak>
30 33  
... ... @@ -72,7 +75,6 @@
72 75 <!-- endinject -->
73 76 <!-- endbuild -->
74 77 <script defer="defer" src="http://barra.brasil.gov.br/barra.js" type="text/javascript"></script>
75   - <script defer="defer" src="http://captcha2.servicoscorporativos.serpro.gov.br/js/captcha.serpro.gov.br.js"></script>
76 78  
77 79 <!-- INJECT-GOOGLE-ANALYTICS -->
78 80 </body>
... ...