diff --git a/src/ppolicy/html/policy.tpl b/src/ppolicy/html/policy.tpl
new file mode 100644
index 0000000..ebb484b
--- /dev/null
+++ b/src/ppolicy/html/policy.tpl
@@ -0,0 +1,129 @@
+{if $pwd_show_policy === "onerror" and !$pwd_show_policy_onerror }
+{else}
+
+ {$msg_policy|unescape: "html" nofilter}
+
+
+
+ {if $pwd_min_length }
+ -
+
+ {$msg_policyminlength|unescape: "html" nofilter} {$pwd_min_length}
+
+ {/if}
+
+
+ {if $pwd_max_length }
+ -
+
+ {$msg_policymaxlength|unescape: "html" nofilter} {$pwd_max_length}
+
+ {/if}
+
+
+ {if $pwd_min_lower }
+ -
+
+ {$msg_policyminlower|unescape: "html" nofilter} {$pwd_min_lower}
+
+ {/if}
+
+
+ {if $pwd_min_upper }
+ -
+
+ {$msg_policyminupper|unescape: "html" nofilter} {$pwd_min_upper}
+
+ {/if}
+
+
+ {if $pwd_min_digit }
+ -
+
+ {$msg_policymindigit|unescape: "html" nofilter} {$pwd_min_digit}
+
+ {/if}
+
+
+ {if $pwd_min_special }
+ -
+
+ {$msg_policyminspecial|unescape: "html" nofilter} {$pwd_min_special}
+
+ {/if}
+
+
+ {if $pwd_complexity }
+ -
+
+ {$msg_policycomplex|unescape: "html" nofilter} {$pwd_complexity}
+
+ {/if}
+
+
+ {if $pwd_forbidden_chars }
+ -
+
+ {$msg_policyforbiddenchars|unescape: "html" nofilter} {$pwd_forbidden_chars}
+
+ {/if}
+
+
+ {if $pwd_diff_last_min_chars }
+ -
+
+ {$msg_policydiffminchars|unescape: "html" nofilter} {$pwd_diff_last_min_chars}
+
+ {/if}
+
+
+ {if $pwd_no_reuse }
+ -
+
+ {$msg_policynoreuse|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $pwd_diff_login }
+ -
+
+ {$msg_policydifflogin|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $use_pwnedpasswords }
+ -
+
+ {$msg_policypwned|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $pwd_no_special_at_ends }
+ -
+
+ {$msg_policyspecialatends|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $pwd_display_entropy }
+ -
+
+ {if $pwd_check_entropy }
+ {$msg_policyentropy|unescape: "html" nofilter}
+ {else}
+ {$msg_policyentropy|unescape: "html" nofilter}
+ {/if}
+
+
+
+ {/if}
+
+
+
+{/if}
diff --git a/src/ppolicy/js/ppolicy.js b/src/ppolicy/js/ppolicy.js
new file mode 100644
index 0000000..aaabc36
--- /dev/null
+++ b/src/ppolicy/js/ppolicy.js
@@ -0,0 +1,376 @@
+(function() {
+ var barWidth, bootstrapClasses, displayEntropyBar, displayEntropyBarMsg, ppolicyResults;
+
+ ppolicyResults = {};
+
+ bootstrapClasses = new Map([["Err", "bg-danger"], ["0", "bg-danger"], ["1", "bg-warning"], ["2", "bg-info"], ["3", "bg-primary"], ["4", "bg-success"]]);
+
+ barWidth = new Map([["Err", "0"], ["0", "20"], ["1", "40"], ["2", "60"], ["3", "80"], ["4", "100"]]);
+
+ json_policy = $("#json-policy").data('policy');
+ var local_policy = JSON.parse(atob(json_policy));
+
+ displayEntropyBar = function(level) {
+ $("#entropybar div").removeClass();
+ $("#entropybar div").addClass('progress-bar');
+ $("#entropybar div").width(barWidth.get(level) + '%');
+ $("#entropybar div").addClass(bootstrapClasses.get(level));
+ return $("#entropybar div").html(barWidth.get(level) + '%');
+ };
+
+ displayEntropyBarMsg = function(msg) {
+ $("#entropybar-msg").html(msg);
+ if (msg.length === 0) {
+ return $("#entropybar-msg").addClass("entropyHidden");
+ } else {
+ return $("#entropybar-msg").removeClass("entropyHidden");
+ }
+ };
+
+ setResult = function(field, result) {
+ var ref, ref1;
+ ppolicyResults[field] = result;
+ $("#" + field).removeClass('fa-times fa-check fa-spinner fa-pulse fa-info-circle fa-question-circle text-danger text-success text-info text-secondary');
+ $("#" + field).attr('role', 'status');
+ switch (result) {
+ case "good":
+ $("#" + field).addClass('fa-check text-success');
+ break;
+ case "bad":
+ $("#" + field).addClass('fa-times text-danger');
+ $("#" + field).attr('role', 'alert');
+ break;
+ case "unknown":
+ $("#" + field).addClass('fa-question-circle text-secondary');
+ break;
+ case "waiting":
+ $("#" + field).addClass('fa-spinner fa-pulse text-secondary');
+ break;
+ case "info":
+ $("#" + field).addClass('fa-info-circle text-info');
+ }
+ if (Object.values(ppolicyResults).every((function(_this) {
+ return function(value) {
+ return value === "good" || value === "info";
+ };
+ })(this))) {
+ $('.ppolicy').removeClass('border-danger').addClass('border-success');
+ return (ref = $('#newpassword').get(0)) != null ? ref.setCustomValidity('') : void 0;
+ } else {
+ $('.ppolicy').removeClass('border-success').addClass('border-danger');
+ return (ref1 = $('#newpassword').get(0)) != null ? ref1.setCustomValidity("Insufficient quality") : void 0;
+ }
+ };
+
+ similar_text = function(first, second, percent) {
+ // discuss at: https://locutus.io/php/similar_text/
+ // original by: RafaĆ Kukawski (https://blog.kukawski.pl)
+ // bugfixed by: Chris McMacken
+ // bugfixed by: Jarkko Rantavuori original by findings in stackoverflow (https://stackoverflow.com/questions/14136349/how-does-similar-text-work)
+ // improved by: Markus Padourek (taken from https://www.kevinhq.com/2012/06/php-similartext-function-in-javascript_16.html)
+ // MIT licenses
+ // example 1: similar_text('Hello World!', 'Hello locutus!')
+ // returns 1: 8
+ // example 2: similar_text('Hello World!', null)
+ // returns 2: 0
+
+ if (first === null ||
+ second === null ||
+ typeof first === 'undefined' ||
+ typeof second === 'undefined') {
+ return 0
+ }
+
+ first += ''
+ second += ''
+
+ let pos1 = 0
+ let pos2 = 0
+ let max = 0
+ const firstLength = first.length
+ const secondLength = second.length
+ let p
+ let q
+ let l
+ let sum
+
+ for (p = 0; p < firstLength; p++) {
+ for (q = 0; q < secondLength; q++) {
+ for (l = 0; (p + l < firstLength) && (q + l < secondLength) && (first.charAt(p + l) === second.charAt(q + l)); l++) {
+ // @todo: ^-- break up this crazy for loop and put the logic in its body
+ }
+ if (l > max) {
+ max = l
+ pos1 = p
+ pos2 = q
+ }
+ }
+ }
+
+ sum = max
+
+ if (sum) {
+ if (pos1 && pos2) {
+ sum += similar_text(first.substr(0, pos1), second.substr(0, pos2))
+ }
+
+ if ((pos1 + max < firstLength) && (pos2 + max < secondLength)) {
+ sum += similar_text(
+ first.substr(pos1 + max, firstLength - pos1 - max),
+ second.substr(pos2 + max,
+ secondLength - pos2 - max))
+ }
+ }
+
+ if (!percent) {
+ return sum
+ }
+
+ return (sum * 200) / (firstLength + secondLength)
+ }
+
+
+ // Generic feature for checkpassword action
+ // check all local policy criteria one by one and display an appropriate button for each
+ $(document).on('checkpassword', function(event, context) {
+ var digit, evType, hasforbidden, i, len, lower, numspechar, password, report, setResult, upper;
+ password = context.password;
+ evType = context.evType;
+ setResult = context.setResult;
+ report = function(result, id) {
+ if (result) {
+ return setResult(id, "good");
+ } else {
+ return setResult(id, "bad");
+ }
+ };
+
+ removePPolicyCriteria = function(criteria, feedback) {
+ // first consider the criteria as fullfilled
+ report( true , feedback);
+ // remove criteria from the list of ppolicy checks
+ delete local_policy[criteria];
+ // remove the tag parent to given feedback
+ $( "#" + feedback ).parent().remove();
+ };
+
+
+ // Criteria checks
+ if (local_policy.pwd_min_length > 0) {
+ report(password.length >= local_policy.pwd_min_length, 'ppolicy-pwd_min_length-feedback');
+ }
+
+ if (local_policy.pwd_max_length > 0) {
+ report(password.length <= local_policy.pwd_max_length, 'ppolicy-pwd_max_length-feedback');
+ }
+
+ if (local_policy.pwd_min_upper > 0) {
+ upper = password.match(/[A-Z]/g);
+ report(upper && upper.length >= local_policy.pwd_min_upper, 'ppolicy-pwd_min_upper-feedback');
+ }
+
+ if (local_policy.pwd_min_lower > 0) {
+ lower = password.match(/[a-z]/g);
+ report(lower && lower.length >= local_policy.pwd_min_lower, 'ppolicy-pwd_min_lower-feedback');
+ }
+
+ if (local_policy.pwd_min_digit > 0) {
+ digit = password.match(/[0-9]/g);
+ report(digit && digit.length >= local_policy.pwd_min_digit, 'ppolicy-pwd_min_digit-feedback');
+ }
+
+ if (local_policy.pwd_no_reuse && local_policy.pwd_no_reuse == true) {
+ if( $( "#oldpassword" ).length )
+ {
+ oldpassword = $( "#oldpassword" ).val();
+ report( password != oldpassword , 'ppolicy-pwd_no_reuse-feedback');
+ }
+ else
+ {
+ removePPolicyCriteria("pwd_no_reuse", 'ppolicy-pwd_no_reuse-feedback');
+ }
+ }
+
+ if (local_policy.pwd_diff_login && local_policy.pwd_diff_login == true) {
+ if( $( "#login" ).length )
+ {
+ login = $( "#login" ).val();
+ report( password != login, 'ppolicy-pwd_diff_login-feedback');
+ }
+ else
+ {
+ report( true , 'ppolicy-pwd_diff_login-feedback');
+ }
+ }
+
+ if (local_policy.pwd_diff_last_min_chars > 0) {
+ if( $( "#oldpassword" ).length )
+ {
+ minDiffChars = local_policy.pwd_diff_last_min_chars;
+ oldpassword = $( "#oldpassword" ).val();
+
+ similarities = similar_text(oldpassword, password);
+ check_len = oldpassword.length < password.length ? oldpassword.length : password.length;
+ new_chars = check_len - similarities;
+ report( new_chars > minDiffChars , 'ppolicy-pwd_diff_last_min_chars-feedback');
+ }
+ else
+ {
+ removePPolicyCriteria("pwd_diff_last_min_chars", 'ppolicy-pwd_diff_last_min_chars-feedback');
+ }
+ }
+
+ if (local_policy.pwd_forbidden_chars) {
+ forbiddenChars = local_policy.pwd_forbidden_chars;
+ forbidden = false;
+ i = 0;
+ while (i < password.length) {
+ if (forbiddenChars.indexOf(password.charAt(i)) != -1) {
+ forbidden = true;
+ }
+ i++;
+ }
+ report( !forbidden, 'ppolicy-pwd_forbidden_chars-feedback' );
+ }
+
+ if (local_policy.pwd_min_special > 0 && local_policy.pwd_special_chars) {
+ numspechar = 0;
+ var re = new RegExp("["+local_policy.pwd_special_chars+"]","");
+ i = 0;
+ while (i < password.length) {
+ if (password.charAt(i).match(re)) {
+ numspechar++;
+ }
+ i++;
+ }
+ report(numspechar >= local_policy.pwd_min_special, 'ppolicy-pwd_min_special-feedback');
+ }
+
+ if ( local_policy.pwd_no_special_at_ends &&
+ local_policy.pwd_no_special_at_ends == true &&
+ local_policy.pwd_special_chars ) {
+ var re_start = new RegExp("^["+local_policy.pwd_special_chars+"]","");
+ var re_end = new RegExp("["+local_policy.pwd_special_chars+"]$","");
+ report( ( !password.match(re_start) && !password.match(re_end) ) , 'ppolicy-pwd_no_special_at_ends-feedback');
+ }
+
+ if ( local_policy.pwd_complexity) {
+ complexity = 0;
+ if (local_policy.pwd_special_chars) {
+ var re = new RegExp("["+local_policy.pwd_special_chars+"]","");
+ if( password.match(re) ){
+ complexity++;
+ }
+ }
+ if( password.match(/[A-Z]/g) ){
+ complexity++;
+ }
+ if( password.match(/[a-z]/g) ){
+ complexity++;
+ }
+ if( password.match(/[0-9]/g) ){
+ complexity++;
+ }
+ report( complexity >= local_policy.pwd_complexity, 'ppolicy-pwd_complexity-feedback');
+ }
+
+
+ if ( local_policy.use_pwnedpasswords) {
+ setResult('ppolicy-use_pwnedpasswords-feedback', "info");
+ }
+
+ });
+
+
+
+ // Specific feature for checkentropy action
+ $(document).on('checkpassword', function(event, context) {
+ var entropyrequired, entropyrequiredlevel, evType, newpasswordVal, password, setResult;
+ password = context.password;
+ evType = context.evType;
+ setResult = context.setResult;
+ if ($('#ppolicy-checkentropy-feedback').length > 0) {
+ newpasswordVal = $("#newpassword").val();
+ entropyrequired = $("span[trspan='checkentropyLabel']").attr("data-checkentropy_required");
+ entropyrequiredlevel = $("span[trspan='checkentropyLabel']").attr("data-checkentropy_required_level");
+ if (newpasswordVal.length === 0) {
+ displayEntropyBar("Err");
+ displayEntropyBarMsg("");
+ setResult('ppolicy-checkentropy-feedback', "unknown");
+ }
+ if (newpasswordVal.length > 0) {
+ return $.ajax({
+ dataType: "json",
+ url: location.pathname + "?action=checkentropy",
+ method: "POST",
+ data: { "password": btoa(newpasswordVal) },
+ context: document.body,
+ success: function(data) {
+ var level, msg;
+ level = data.level;
+ msg = data.message;
+ if (level !== void 0) {
+ if (parseInt(level) >= 0 && parseInt(level) <= 4) {
+ displayEntropyBar(level);
+ displayEntropyBarMsg(msg);
+ if (entropyrequired === "1" && entropyrequiredlevel.length > 0) {
+ if (parseInt(level) >= parseInt(entropyrequiredlevel)) {
+ setResult('ppolicy-checkentropy-feedback', "good");
+ } else {
+ setResult('ppolicy-checkentropy-feedback', "bad");
+ }
+ }
+ if (entropyrequired !== "1") {
+ return setResult('ppolicy-checkentropy-feedback', "good");
+ }
+ } else if (parseInt(level) === -1) {
+ displayEntropyBar(level);
+ displayEntropyBarMsg(msg);
+ return setResult('ppolicy-checkentropy-feedback', "bad");
+ } else {
+ displayEntropyBar(level);
+ displayEntropyBarMsg(msg);
+ return setResult('ppolicy-checkentropy-feedback', "unknown");
+ }
+ }
+ },
+ error: function(j, status, err) {
+ var res;
+ if (err) {
+ console.log('checkentropy: frontend error: ', err);
+ }
+ if (j) {
+ res = JSON.parse(j.responseText);
+ }
+ if (res && res.error) {
+ return console.log('checkentropy: returned error: ', res);
+ }
+ }
+ });
+ }
+ }
+ });
+
+
+
+ checkpassword = function(password, evType) {
+ var e, info;
+ e = jQuery.Event("checkpassword");
+ info = {
+ password: password,
+ evType: evType,
+ setResult: setResult
+ };
+ return $(document).trigger(e, info);
+ };
+ if ( (local_policy != null) && $('#newpassword').length) {
+ checkpassword('');
+ $('#newpassword').keyup(function(e) {
+ checkpassword(e.target.value);
+ });
+ $('#newpassword').focusout(function(e) {
+ checkpassword(e.target.value, "focusout");
+ });
+ }
+
+}).call(this);