diff --git a/src/Snaptcha.php b/src/Snaptcha.php index 6653840..1221e3c 100644 --- a/src/Snaptcha.php +++ b/src/Snaptcha.php @@ -8,7 +8,6 @@ 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; @@ -16,7 +15,6 @@ use putyourlightson\snaptcha\variables\SnaptchaVariable; use yii\base\ActionEvent; use yii\base\Event; -use yii\web\ForbiddenHttpException; /** * @property SnaptchaService $snaptcha diff --git a/src/migrations/Install.php b/src/migrations/Install.php index fa36984..ea7a281 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -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 @@ -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(), @@ -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; } diff --git a/src/migrations/m210216_120000_migrate_settings.php b/src/migrations/m210216_120000_migrate_settings.php index 499c6d1..5ca04cc 100644 --- a/src/migrations/m210216_120000_migrate_settings.php +++ b/src/migrations/m210216_120000_migrate_settings.php @@ -7,6 +7,7 @@ use Craft; use craft\db\Migration; +use craft\helpers\StringHelper; use putyourlightson\snaptcha\models\SettingsModel; use putyourlightson\snaptcha\Snaptcha; @@ -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; } diff --git a/src/migrations/m210217_120000_add_value_column.php b/src/migrations/m210217_120000_add_value_column.php new file mode 100644 index 0000000..7a65a05 --- /dev/null +++ b/src/migrations/m210217_120000_add_value_column.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/src/models/SettingsModel.php b/src/models/SettingsModel.php index cb38d00..b252544 100644 --- a/src/models/SettingsModel.php +++ b/src/models/SettingsModel.php @@ -29,6 +29,11 @@ class SettingsModel extends Model */ public $fieldName = 'snaptcha'; + /** + * @var string + */ + public $salt = ''; + /** * @var string */ @@ -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 = []; diff --git a/src/models/SnaptchaModel.php b/src/models/SnaptchaModel.php index 1f5a174..b1ca079 100644 --- a/src/models/SnaptchaModel.php +++ b/src/models/SnaptchaModel.php @@ -14,6 +14,11 @@ class SnaptchaModel extends Model */ public $key; + /** + * @var string + */ + public $value; + /** * @var string */ @@ -40,7 +45,7 @@ class SnaptchaModel extends Model public function rules(): array { return [ - [['key', 'ipAddress'], 'required'], + [['key', 'value', 'ipAddress'], 'required'], [['timestamp', 'expirationTime', 'minimumSubmitTime'], 'integer'], ]; } diff --git a/src/records/SnaptchaRecord.php b/src/records/SnaptchaRecord.php index a41d393..4dc3e42 100755 --- a/src/records/SnaptchaRecord.php +++ b/src/records/SnaptchaRecord.php @@ -10,6 +10,7 @@ /** * @property int $id * @property string $key + * @property string $value * @property string $ipAddress * @property int $timestamp * @property int|null $expirationTime diff --git a/src/services/SnaptchaService.php b/src/services/SnaptchaService.php index 323a16b..c1cf14c 100644 --- a/src/services/SnaptchaService.php +++ b/src/services/SnaptchaService.php @@ -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; } /** @@ -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(); @@ -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); } /** @@ -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'); + } + } } diff --git a/src/templates/_error.html b/src/templates/_error.html index caafd72..aacef1f 100644 --- a/src/templates/_error.html +++ b/src/templates/_error.html @@ -21,7 +21,7 @@