pax_global_header 0000666 0000000 0000000 00000000064 14555513613 0014522 g ustar 00root root 0000000 0000000 52 comment=3d5d2af8f354a0b679a6ad75dd1fbe4ce92dca8b
.gitattributes 0000664 0000000 0000000 00000000300 14555513613 0013752 0 ustar 00root root 0000000 0000000 # Force our line endings to be LF, even for Windows
* text=auto
# Set certain files to be binary
*.png binary
*.jpg binary
*.gif binary
*.tgz binary
*.zip binary
*.tar.gz binary
*.ttf binary
.github/ 0000775 0000000 0000000 00000000000 14555513613 0012426 5 ustar 00root root 0000000 0000000 .github/workflows/ 0000775 0000000 0000000 00000000000 14555513613 0014463 5 ustar 00root root 0000000 0000000 .github/workflows/php.yml 0000664 0000000 0000000 00000002333 14555513613 0015776 0 ustar 00root root 0000000 0000000 name: PHP Check
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
php: [ 8.0, 8.1, 8.2, 8.3 ]
name: PHP ${{ matrix.php }} Syntax Check
steps:
- uses: actions/checkout@v3
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-php-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --ansi
- name: Lint PHP files
run: vendor/overtrue/phplint/bin/phplint -w --exclude .git --exclude vendor --ansi .
csfixer:
runs-on: ubuntu-latest
name: PHP CS Fixer
steps:
- uses: actions/checkout@v3
- name: luminsports-php-cs-fixer
uses: luminsports/github-action-php-cs-fixer@main
with:
php-cs-fixer-version: "v3.46.0"
use-built-in-rules: false
.gitignore 0000664 0000000 0000000 00000000035 14555513613 0013054 0 ustar 00root root 0000000 0000000 /vendor/
.php-cs-fixer.cache
.php-cs-fixer.dist.php 0000664 0000000 0000000 00000010102 14555513613 0015116 0 ustar 00root root 0000000 0000000 in(__DIR__)
// Don't touch libraries.
->exclude([
'cache',
'other',
'Packages',
'Smileys',
'Sources/minify',
'Sources/random_compat',
'Sources/ReCaptcha',
'Themes',
])
// Skip all index.php files and ssi_example.php.
->notName(['index.php', 'ssi_examples.php'])
// Skip anything being ignored in .gitignore.
->ignoreVCSIgnored(true);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
// PSR12 overrides.
'no_closing_tag' => false,
'no_break_comment' => false, // A bit buggy with comments.
'statement_indentation' => false, // A bit buggy with comments.
// Array notation.
'array_syntax' => ['syntax' => 'short'],
'normalize_index_brace' => true,
'whitespace_after_comma_in_array' => true,
// Basic.
'no_trailing_comma_in_singleline' => true,
// Casing.
'class_reference_name_casing' => true,
// Cast notation.
'cast_spaces' => ['space' => 'single'],
// Control structure.
'include' => true,
'no_superfluous_elseif' => true,
'no_useless_else' => true,
'simplified_if_return' => true,
'trailing_comma_in_multiline' => [
'after_heredoc' => true,
'elements' => [
'arguments',
'arrays',
'match',
'parameters',
],
],
// Function notation.
'lambda_not_used_import' => true,
'nullable_type_declaration_for_default_null_value' => true,
// Import.
'no_unused_imports' => true,
'ordered_imports' => [
'imports_order' => [
'class',
'function',
'const',
],
'sort_algorithm' => 'alpha',
],
// Language construct.
'combine_consecutive_issets' => true,
'combine_consecutive_unsets' => true,
'nullable_type_declaration' => ['syntax' => 'question_mark'],
// Namespace notation.
'no_leading_namespace_whitespace' => true,
// Operator.
'concat_space' => ['spacing' => 'one'],
'operator_linebreak' => [
'only_booleans' => true,
'position' => 'beginning',
],
'standardize_not_equals' => true,
'ternary_to_null_coalescing' => true,
// PHPDoc.
'phpdoc_indent' => true,
'phpdoc_line_span' => [
'const' => 'multi',
'property' => 'multi',
'method' => 'multi',
],
'phpdoc_no_access' => true,
'phpdoc_no_useless_inheritdoc' => true,
'phpdoc_order' => [
'order' => [
'param',
'throws',
'return',
],
],
'phpdoc_no_empty_return' => true,
'phpdoc_param_order' => true,
'phpdoc_scalar' => [
'types' => [
'boolean',
'callback',
'double',
'integer',
'real',
'str',
],
],
'phpdoc_to_comment' => [
'ignored_tags' => ['todo'],
],
'phpdoc_trim_consecutive_blank_line_separation' => true,
'phpdoc_types' => [
'groups' => ['alias', 'meta', 'simple'],
],
'phpdoc_var_without_name' => true,
// Return notation.
'no_useless_return' => true,
'simplified_null_return' => true,
// Semicolon.
'multiline_whitespace_before_semicolons' => true,
'no_empty_statement' => true,
'no_singleline_whitespace_before_semicolons' => true,
// String notation.
'explicit_string_variable' => true,
'simple_to_complex_string_variable' => true,
'single_quote' => true,
// Whitespace.
'array_indentation' => true,
'blank_line_before_statement' => [
'statements' => [
'case',
'continue',
'declare',
'default',
'do',
'exit',
'for',
'foreach',
'goto',
'if',
'include',
'include_once',
'require',
'require_once',
'return',
'switch',
'throw',
'try',
'while',
'yield',
'yield_from',
],
],
'heredoc_indentation' => ['indentation' => 'start_plus_one'],
'method_chaining_indentation' => true,
'no_spaces_around_offset' => [
'positions' => ['inside', 'outside'],
],
'type_declaration_spaces' => [
'elements' => ['function', 'property'],
],
])
->setIndent("\t")
->setFinder($finder);
?> .scrutinizer.yml 0000664 0000000 0000000 00000013441 14555513613 0014253 0 ustar 00root root 0000000 0000000 checks:
php:
variable_existence: true
use_statement_alias_conflict: true
unused_variables: true
unused_properties: true
unused_parameters: true
unused_methods: true
unreachable_code: true
switch_fallthrough_commented: true
simplify_boolean_return: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
require_scope_for_methods: true
require_php_tag_first: true
remove_extra_empty_lines: true
property_assignments: true
precedence_mistakes: true
precedence_in_conditions: true
parse_doc_comments: true
parameter_non_unique: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
overriding_private_members: true
no_trailing_whitespace: true
no_short_open_tag: true
no_property_on_interface: true
no_non_implemented_abstract_methods: true
no_short_method_names:
minimum: '3'
no_goto: true
no_error_suppression: true
no_debug_code: true
more_specific_types_in_doc_comments: true
missing_arguments: true
method_calls_on_non_object: true
instanceof_class_exists: true
foreach_traversable: true
fix_use_statements:
remove_unused: true
preserve_multiple: false
preserve_blanklines: false
order_alphabetically: false
fix_line_ending: true
fix_doc_comments: true
encourage_shallow_comparison: true
duplication: true
deprecated_code_usage: true
deadlock_detection_in_loops: true
code_rating: true
closure_use_not_conflicting: true
closure_use_modifiable: true
catch_class_exists: true
avoid_duplicate_types: true
avoid_closing_tag: false
assignment_of_null_return: true
argument_type_checks: true
no_long_variable_names:
maximum: '40'
no_short_variable_names:
minimum: '3'
phpunit_assertions: true
remove_php_closing_tag: false
no_mixed_inline_html: false
require_braces_around_control_structures: false
psr2_control_structure_declaration: false
avoid_superglobals: false
security_vulnerabilities: false
no_exit: false
coding_style:
php:
indentation:
general:
use_tabs: true
size: 4
switch:
indent_case: true
spaces:
general:
linefeed_character: newline
before_parentheses:
function_declaration: false
closure_definition: false
function_call: false
if: true
for: true
while: true
switch: true
catch: true
array_initializer: false
around_operators:
assignment: true
logical: true
equality: true
relational: true
bitwise: true
additive: true
multiplicative: true
shift: true
unary_additive: false
concatenation: true
negation: false
before_left_brace:
class: true
function: true
if: true
else: true
for: true
while: true
do: true
switch: true
try: true
catch: true
finally: true
before_keywords:
else: true
while: true
catch: true
finally: true
within:
brackets: false
array_initializer: false
grouping: false
function_call: false
function_declaration: false
if: false
for: false
while: false
switch: false
catch: false
type_cast: false
ternary_operator:
before_condition: true
after_condition: true
before_alternative: true
after_alternative: true
in_short_version: false
other:
before_comma: false
after_comma: true
before_semicolon: false
after_semicolon: true
after_type_cast: true
braces:
classes_functions:
class: new-line
function: new-line
closure: new-line
if:
opening: new-line
always: false
else_on_new_line: true
for:
opening: new-line
always: false
while:
opening: new-line
always: false
do_while:
opening: undefined
always: true
while_on_new_line: true
switch:
opening: new-line
try:
opening: new-line
catch_on_new_line: true
finally_on_new_line: true
upper_lower_casing:
keywords:
general: lower
constants:
true_false_null: lower
build:
nodes:
analysis:
tests:
override:
- php-scrutinizer-run
dependencies:
after:
- git clone -b release-2.1 https://github.com/SimpleMachines/SMF smf
filter:
dependency_paths:
- smf/
excluded_paths:
- '*.min.js'
DCO.txt 0000664 0000000 0000000 00000002334 14555513613 0012236 0 ustar 00root root 0000000 0000000 Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved. LICENSE 0000664 0000000 0000000 00000002752 14555513613 0012101 0 ustar 00root root 0000000 0000000 BSD 3-Clause License
Copyright (c) 2024, SleePy
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md 0000664 0000000 0000000 00000000507 14555513613 0012347 0 ustar 00root root 0000000 0000000 This enables checking passwords against the [Have-I-Been-Pwned](https://haveibeenpwned.com/Passwords) database. Passwords are only checked on registration and when changed on the profile.
Additionally this can attempt to check the password from the browser using the same API
SMF 2.1.0 or higher only!
PHP 7.3 or higher only SMF2.1/ 0000775 0000000 0000000 00000000000 14555513613 0011734 5 ustar 00root root 0000000 0000000 SMF2.1/Hibp.php 0000664 0000000 0000000 00000014461 14555513613 0013335 0 ustar 00root root 0000000 0000000
* @copyright 2022
* @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause
* @version 1.0
*/
/**
* Checks the password against the Have-I-Been-Pwned database.
*
* @param string $password The password to check
* @param bool $hashed If the password is hashed already or not.
* @return bool The result if found or not. If null, the check failed.
*/
function hibp_password(string $password, bool $hashed = false): ?bool
{
global $smcFunc;
if (!$hashed) {
$password = sha1($password);
}
$passhash_prefix = $smcFunc['substr']($password, 0, 5);
$passhash_suffix = $smcFunc['substr']($password, 5);
$call_url = 'https://api.pwnedpasswords.com/range/' . $passhash_prefix;
/*
* SMF's fetch_web_data doesn't support sending headers, so we are limited in our API options
* and could build our own to do this, but this works fine.
*/
$results = fetch_web_data($call_url);
// Invalid results, just pass them through.
if (empty($results)) {
return null;
}
// Sure we could make an array of the data, but we just want to see if its found.
$found = preg_match(
'~\s+' . preg_quote($passhash_suffix) . ':\d+~i',
$results,
);
// We found a result, its found.
if ($found === 1) {
return true;
}
// No result, return false.
if ($found === 0) {
return false;
}
// $found returned something invalid, also fail.
return null;
}
/**
* Checks the password against the HiBP database.
*
* @calledby call_integration_hook('integrate_validatePassword', array($password, $username, $restrict_in, &$reg_error));
* @param string $password The password to check
* @param string $username Currently ignored by this hook.
* @param array $restrict_in Currently ignored by this hook.
* @param string $pass_error A password error if any. If this is set, we won't process our hook.
*/
function hibp_validatePassword(string $password, string $username, array $restrict_in, string &$pass_error): void
{
global $modSettings;
// If another hook has set this, leave it alone.
if (!empty($pass_error) || empty($modSettings['enableHibP'])) {
return;
}
// Send it to the backend.
$res = hibp_password($password);
// If the result is true, we want to present a error to the prefix of $txt['profile_error_password_*']
if ($res === true) {
loadLanguage('Hibp');
$pass_error = 'hibp';
}
}
/**
* When the password field is setup on the registration, send in some javascript.
*
* @calledby call_integration_hook('integrate_load_custom_profile_fields', array($memID, $area));
* @param array $fields User profile fields we are loading.
*/
function hibp_load_custom_profile_fields(int $memID, string $area): void
{
global $modSettings;
if ($area !== 'register' || empty($modSettings['enableHibPjs'])) {
return;
}
//
hibp_build_javascript('#smf_autov_pwmain', '#smf_autov_pwmain_div');
}
/**
* When the password field is setup on the profile pages, send in some javascript.
*
* @calledby call_integration_hook('integrate_setup_profile_context', array(&$fields));
* @param array $fields User profile fields we are loading.
*/
function hibp_setup_profile_context(array $fields): void
{
global $modSettings;
// If we are not loading the password field, don't bother.
if (!in_array('passwrd1', $fields) || empty($modSettings['enableHibPjs'])) {
return;
}
//
hibp_build_javascript('#passwrd1', '#passwrd1');
}
function hibp_build_javascript(string $selector, string $errorSelector): void
{
global $txt;
loadLanguage('Hibp');
// Add the JS. From: https://github.com/emn178/js-sha1
loadJavaScriptFile('sha1.js');
/*
* When the password is ok we use:
* When the password is bad we use:
*/
addInlineJavaScript('
$(document).ready(function () {
$(' . JavaScriptEscape($selector) . ').on("change", function (e){
var $hibp_attachSelector = ' . JavaScriptEscape($errorSelector) . '
var $passhash = sha1($(this).val());
var $passhash_prefix = $passhash.substring(0, 5);
var $passhash_suffix = $passhash.substring(5);
var $passhash_regx = new RegExp("\\\\s+" + $passhash_suffix + ":\\\\d+", "i");
$.ajax({
url: "https://api.pwnedpasswords.com/range/" + $passhash_prefix,
type: "GET",
dataType: "html",
success: function (data, textStatus, xhr) {
var $res = $passhash_regx.test(data);
// Build the box.
if (typeof $hibpBox === "undefined")
$hibpBox = $($hibp_attachSelector).parent().append(' . JavaScriptEscape('
' . $txt['profile_error_password_hibp'] . '
') . ');
// It was found.
if ($res === true)
$($hibpBox).find("div.errorbox").show();
else if ($res === false)
$($hibpBox).find("div.errorbox").hide();
}
});
});
});
');
/*
test
*/
}
/**
* Adds Hibp options to the mangage registration.
* General registration settings and Coppa compliance settings.
* Accessed by ?action=admin;area=regcenter;sa=settings.
* Requires the admin_forum permission.
*
* @param bool $return_config Whether or not to return the config_vars array (used for admin search)
* @return void|array Returns nothing or returns the $config_vars array if $return_config is true
*/
function hibp_general_security_settings(array &$config_vars): void
{
global $txt;
loadLanguage('Hibp');
// Find the last password setting.
foreach ($config_vars as $id => $val) {
if (is_array($val) && $val[1] == 'enable_password_conversion' && is_string($config_vars[$id + 1]) && $config_vars[$id + 1] == '') {
break;
}
}
$varsA = array_slice($config_vars, 0, $id + 1);
$varsB = array_slice($config_vars, $id + 1);
$new_vars = [
'',
['check', 'enableHibP'],
['check', 'enableHibPjs'],
];
$config_vars = array_merge($varsA, $new_vars, $varsB);
// Saving?
if (isset($_GET['save'])) {
// Can't have one without the other.
if (!empty($_POST['enableHibPjs']) && empty($_POST['enableHibP'])) {
$_POST['enableHibP'] = $_POST['enableHibPjs'];
}
}
}
SMF3.0/ 0000775 0000000 0000000 00000000000 14555513613 0011734 5 ustar 00root root 0000000 0000000 SMF3.0/Hibp.php 0000664 0000000 0000000 00000015070 14555513613 0013332 0 ustar 00root root 0000000 0000000
* @copyright 2024
* @license 3-Clause BSD https://opensource.org/licenses/BSD-3-Clause
* @version 1.0
*/
#namespace SMF\Mod\ErrorPopup;
use SMF\Config;
use SMF\Lang;
use SMF\Theme;
use SMF\User;
use SMF\Utils;
use SMF\WebFetch\WebFetchApi;
class hibp
{
/**
* Checks the password against the Have-I-Been-Pwned database.
*
* @param string $password The password to check
* @param bool $hashed If the password is hashed already or not.
* @return bool The result if found or not. If null, the check failed.
*/
public static function checkPassword(string $password, bool $hashed = false): ?bool
{
if (!$hashed) {
$password = sha1($password);
}
$passhash_prefix = Utils::entitySubstr($password, 0, 5);
$passhash_suffix = Utils::entitySubstr($password, 5);
$call_url = 'https://api.pwnedpasswords.com/range/' . $passhash_prefix;
/*
* SMF's fetch_web_data doesn't support sending headers, so we are limited in our API options
* and could build our own to do this, but this works fine.
*/
$results = WebFetchApi::fetch($call_url);
// Invalid results, just pass them through.
if (empty($results)) {
return null;
}
// Sure we could make an array of the data, but we just want to see if its found.
$found = preg_match(
'~\s+' . preg_quote($passhash_suffix) . ':\d+~i',
$results,
);
// We found a result, its found.
if ($found === 1) {
return true;
}
// No result, return false.
if ($found === 0) {
return false;
}
// $found returned something invalid, also fail.
return null;
}
/**
* Checks the password against the HiBP database.
*
* @calledby call_integration_hook('integrate_validatePassword', array($password, $username, $restrict_in, &$reg_error));
* @param string $password The password to check
* @param string $username Currently ignored by this hook.
* @param array $restrict_in Currently ignored by this hook.
* @param string $pass_error A password error if any. If this is set, we won't process our hook.
*/
public static function validatePassword(string $password, string $username, array $restrict_in, string &$pass_error): void
{
// If another hook has set this, leave it alone.
if (!empty($pass_error) || empty(Config::$modSettings['enableHibP'])) {
return;
}
// Send it to the backend.
$res = self::checkPassword($password);
// If the result is true, we want to present a error to the prefix of $txt['profile_error_password_*']
if ($res === true) {
Lang::load('Hibp');
$pass_error = 'hibp';
}
}
/**
* When the password field is setup on the registration, send in some javascript.
*
* @calledby call_integration_hook('integrate_load_custom_profile_fields', array($memID, $area));
* @param array $fields User profile fields we are loading.
*/
public static function addToRegistrationPage(int $memID, string $area): void
{
if ($area !== 'register' || empty(Config::$modSettings['enableHibPjs'])) {
return;
}
//
self::buildJavascript('#smf_autov_pwmain', '#smf_autov_pwmain_div');
}
/**
* When the password field is setup on the profile pages, send in some javascript.
*
* @calledby call_integration_hook('integrate_setup_profile_context', array(&$fields));
* @param array $fields User profile fields we are loading.
*/
public static function addToProfileContext(array $fields): void
{
global $modSettings;
// If we are not loading the password field, don't bother.
if (!in_array('passwrd1', $fields) || empty($modSettings['enableHibPjs'])) {
return;
}
//
self::buildJavascript('#passwrd1', '#passwrd1');
}
public static function buildJavascript(string $selector, string $errorSelector): void
{
Lang::load('Hibp');
// Add the JS. From: https://github.com/emn178/js-sha1
Theme::loadJavaScriptFile('sha1.js');
/*
* When the password is ok we use:
* When the password is bad we use:
*/
Theme::addInlineJavaScript('
$(document).ready(function () {
$(' . Utils::JavaScriptEscape($selector) . ').on("change", function (e){
var $hibp_attachSelector = ' . Utils::JavaScriptEscape($errorSelector) . '
var $passhash = sha1($(this).val());
var $passhash_prefix = $passhash.substring(0, 5);
var $passhash_suffix = $passhash.substring(5);
var $passhash_regx = new RegExp("\\\\s+" + $passhash_suffix + ":\\\\d+", "i");
$.ajax({
url: "https://api.pwnedpasswords.com/range/" + $passhash_prefix,
type: "GET",
dataType: "html",
success: function (data, textStatus, xhr) {
var $res = $passhash_regx.test(data);
// Build the box.
if (typeof $hibpBox === "undefined")
$hibpBox = $($hibp_attachSelector).parent().append(' . Utils::JavaScriptEscape('