Skip to content

Commit

Permalink
Added hashed value
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Croker committed Feb 17, 2021
1 parent 7ede821 commit 385311a
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 74 deletions.
2 changes: 0 additions & 2 deletions src/Snaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@
use Craft;
use craft\base\Plugin;
use craft\web\Controller;
use craft\web\Request;
use craft\web\twig\variables\CraftVariable;
use craft\web\View;
use putyourlightson\snaptcha\models\SettingsModel;
use putyourlightson\snaptcha\services\SnaptchaService;
use putyourlightson\snaptcha\variables\SnaptchaVariable;
use yii\base\ActionEvent;
use yii\base\Event;
use yii\web\ForbiddenHttpException;

/**
* @property SnaptchaService $snaptcha
Expand Down
18 changes: 13 additions & 5 deletions src/migrations/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

use Craft;
use craft\db\Migration;
use craft\helpers\StringHelper;
use putyourlightson\snaptcha\records\SnaptchaRecord;
use putyourlightson\snaptcha\Snaptcha;

/**
* Install Migration
Expand All @@ -19,12 +21,13 @@ class Install extends Migration
*/
public function safeUp(): bool
{
$snaptchaTable = SnaptchaRecord::tableName();
$table = SnaptchaRecord::tableName();

if (!$this->db->tableExists($snaptchaTable)) {
$this->createTable($snaptchaTable, [
if (!$this->db->tableExists($table)) {
$this->createTable($table, [
'id' => $this->primaryKey(),
'key' => $this->string(),
'value' => $this->string(),
'ipAddress' => $this->string(),
'timestamp' => $this->integer(),
'expirationTime' => $this->integer(),
Expand All @@ -34,13 +37,18 @@ public function safeUp(): bool
'uid' => $this->uid(),
]);

$this->createIndex(null, $snaptchaTable, 'key', false);
$this->createIndex(null, $snaptchaTable, 'ipAddress', false);
$this->createIndex(null, $table, 'value', false);
$this->createIndex(null, $table, 'ipAddress', false);

// Refresh the db schema caches
Craft::$app->db->schema->refresh();
}

// Create and save default settings
$settings = Snaptcha::$plugin->settings;
$settings->salt = StringHelper::randomString(16);
Craft::$app->plugins->savePluginSettings(Snaptcha::$plugin, $settings->getAttributes());

return true;
}

Expand Down
4 changes: 3 additions & 1 deletion src/migrations/m210216_120000_migrate_settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use Craft;
use craft\db\Migration;
use craft\helpers\StringHelper;
use putyourlightson\snaptcha\models\SettingsModel;
use putyourlightson\snaptcha\Snaptcha;

Expand All @@ -26,8 +27,9 @@ public function safeUp(): bool

// Resave plugin settings
$settings = Snaptcha::$plugin->settings;
$settings->salt = StringHelper::randomString(16);

// Only update if original is unchanged
// Only update if original message is unchanged
if ($settings->errorMessage == 'Sorry, you have failed the security test. Please ensure that you have javascript enabled and that you refresh the page that you are trying to submit.') {
$settings->errorMessage = (new SettingsModel())->errorMessage;
}
Expand Down
52 changes: 52 additions & 0 deletions src/migrations/m210217_120000_add_value_column.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
/**
* @copyright Copyright (c) PutYourLightsOn
*/

namespace putyourlightson\snaptcha\migrations;

use Craft;
use craft\db\Migration;
use craft\helpers\MigrationHelper;
use putyourlightson\snaptcha\records\SnaptchaRecord;

class m210217_120000_add_value_column extends Migration
{
/**
* @inheritdoc
*/
public function safeUp(): bool
{
$schemaVersion = Craft::$app->projectConfig
->get('plugins.snaptcha.schemaVersion', true);

if (!version_compare($schemaVersion, '3.0.0', '<')) {
return true;
}

$table = SnaptchaRecord::tableName();

// Delete all rows to avoid having stale values in the DB
$this->delete($table);

if (!$this->db->columnExists($table, 'value')) {
$this->addColumn($table, 'value', $this->string()->after('key'));

$this->createIndex(null, $table, 'value', false);

MigrationHelper::dropIndexIfExists($table, 'key');
}

return true;
}

/**
* @inheritdoc
*/
public function safeDown(): bool
{
echo "m181009_120000_update_blacklist_settings cannot be reverted.\n";

return false;
}
}
11 changes: 8 additions & 3 deletions src/models/SettingsModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class SettingsModel extends Model
*/
public $fieldName = 'snaptcha';

/**
* @var string
*/
public $salt = '';

/**
* @var string
*/
Expand Down Expand Up @@ -65,17 +70,17 @@ class SettingsModel extends Model
public $minimumSubmitTime = 3;

/**
* @var array
* @var array|string
*/
public $excludeControllerActions = [];

/**
* @var array
* @var array|string
*/
public $allowList = [];

/**
* @var array
* @var array|string
*/
public $denyList = [];

Expand Down
7 changes: 6 additions & 1 deletion src/models/SnaptchaModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class SnaptchaModel extends Model
*/
public $key;

/**
* @var string
*/
public $value;

/**
* @var string
*/
Expand All @@ -40,7 +45,7 @@ class SnaptchaModel extends Model
public function rules(): array
{
return [
[['key', 'ipAddress'], 'required'],
[['key', 'value', 'ipAddress'], 'required'],
[['timestamp', 'expirationTime', 'minimumSubmitTime'], 'integer'],
];
}
Expand Down
1 change: 1 addition & 0 deletions src/records/SnaptchaRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/**
* @property int $id
* @property string $key
* @property string $value
* @property string $ipAddress
* @property int $timestamp
* @property int|null $expirationTime
Expand Down
148 changes: 93 additions & 55 deletions src/services/SnaptchaService.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,54 +42,29 @@ class SnaptchaService extends Component
];

/**
* Returns a field value.
* Returns a field key.
*
* @param SnaptchaModel $model
*
* @return string|null
*/
public function getFieldValue(SnaptchaModel $model)
public function getFieldKey(SnaptchaModel $model)
{
$now = time();
$hashedIpAddress = $this->_getHashedIpAddress();

// Get most recent record with IP address from DB
/** @var SnaptchaRecord|null $record */
$record = SnaptchaRecord::find()
->where(['ipAddress' => $hashedIpAddress])
->orderBy('timestamp desc')
->one();

// If record does not exist or one time key is enabled or the expiration time has passed
if ($record === null || Snaptcha::$plugin->settings->oneTimeKey || $record->timestamp + ($record->expirationTime * 60) < $now) {
// Set key to random string
$model->key = StringHelper::randomString();

// Hash IP address for privacy
$model->ipAddress = $hashedIpAddress;

// Set timestamp to current time
$model->timestamp = $now;

// Set optional fields from settings if not defined
$model->expirationTime = $model->expirationTime ?? Snaptcha::$plugin->settings->expirationTime;
$model->minimumSubmitTime = $model->minimumSubmitTime ?? Snaptcha::$plugin->settings->minimumSubmitTime;

if (!$model->validate()) {
return null;
}

$record = new SnaptchaRecord($model);
}
$record = $this->_getSnaptchaRecord($model);

// Refresh timestamp
$record->timestamp = $now;
return $record ? $record->key : null;
}

if (!$record->save()) {
return null;
}
/**
* Returns a field value.
*
* @param SnaptchaModel $model
* @return string|null
*/
public function getFieldValue(SnaptchaModel $model)
{
$record = $this->_getSnaptchaRecord($model);

return $record->key;
return $record ? $record->value : null;
}

/**
Expand Down Expand Up @@ -188,7 +163,7 @@ public function validateField(string $value = null): bool
/** @var SnaptchaRecord|null $record */
$record = SnaptchaRecord::find()
->where([
'key' => $value,
'value' => $value,
'ipAddress' => $this->_getHashedIpAddress(),
])
->one();
Expand Down Expand Up @@ -234,30 +209,78 @@ public function validateField(string $value = null): bool
}

/**
* Returns the current user's hashed IP address.
* Returns a Snaptcha record.
*
* @return string
* @param SnaptchaModel $model
* @return SnaptchaRecord|null
*/
private function _getHashedIpAddress(): string
private function _getSnaptchaRecord(SnaptchaModel $model)
{
$ipAddress = Craft::$app->getRequest()->getUserIP();
$now = time();
$hashedIpAddress = $this->_getHashedIpAddress();

return $ipAddress === null ? '' : md5($ipAddress);
// Get most recent record with IP address from DB
/** @var SnaptchaRecord|null $record */
$record = SnaptchaRecord::find()
->where(['ipAddress' => $hashedIpAddress])
->orderBy('timestamp desc')
->one();

// If record does not exist or one time key is enabled or the expiration time has passed
if ($record === null || Snaptcha::$plugin->settings->oneTimeKey || $record->timestamp + ($record->expirationTime * 60) < $now) {
// Set key to random string
$model->key = StringHelper::randomString(16);
$model->value = $this->_getHashedValue($model->key, Snaptcha::$plugin->settings->salt);

// Hash IP address for privacy
$model->ipAddress = $hashedIpAddress;

// Set timestamp to current time
$model->timestamp = $now;

// Set optional fields from settings if not defined
$model->expirationTime = $model->expirationTime ?? Snaptcha::$plugin->settings->expirationTime;
$model->minimumSubmitTime = $model->minimumSubmitTime ?? Snaptcha::$plugin->settings->minimumSubmitTime;

if (!$model->validate()) {
return null;
}

$record = new SnaptchaRecord($model);
}

// Refresh timestamp
$record->timestamp = $now;

if (!$record->save()) {
return null;
}

return $record;
}

/**
* Rejects and logs a form submission.
* Returns the hashed value.
*
* @param string $message
* @param array $params
* @param string $key
* @param string $salt
* @return string
*/
private function _reject(string $message, array $params = [])
private function _getHashedValue(string $key, string $salt)
{
if (Snaptcha::$plugin->settings->logRejected) {
$url = Craft::$app->getRequest()->getAbsoluteUrl();
$message = Craft::t('snaptcha', $message, $params).' ['.$url.']';
LogToFile::log($message, 'snaptcha');
}
return base64_encode($key.$salt);
}

/**
* Returns the current user's hashed IP address.
*
* @return string
*/
private function _getHashedIpAddress(): string
{
$ipAddress = Craft::$app->getRequest()->getUserIP();

return $ipAddress === null ? '' : md5($ipAddress);
}

/**
Expand All @@ -284,4 +307,19 @@ private function _getNormalizedArray($values): array

return $values;
}

/**
* Rejects and logs a form submission.
*
* @param string $message
* @param array $params
*/
private function _reject(string $message, array $params = [])
{
if (Snaptcha::$plugin->settings->logRejected) {
$url = Craft::$app->getRequest()->getAbsoluteUrl();
$message = Craft::t('snaptcha', $message, $params).' ['.$url.']';
LogToFile::log($message, 'snaptcha');
}
}
}
2 changes: 1 addition & 1 deletion src/templates/_error.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ <h2>{{ errorTitle }}</h2>
</p>

<form action="" method="post">
{% for name, value in postedValues %}
{% for name, value in postedValues if name != 'snaptcha' %}
<input type="hidden" name="{{ name }}" value="{{ value }}">
{% endfor %}

Expand Down
Loading

0 comments on commit 385311a

Please sign in to comment.