jdzcaptcha 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/cli/jpack.js +3 -0
- package/config/jpack.js +129 -0
- package/config/jpack.template +5 -0
- package/config/jpack.wrapper.js +16 -0
- package/dist/assets/jdzcaptcha/placeholder.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-1.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-10.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-11.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-12.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-13.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-14.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-15.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-16.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-17.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-18.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-19.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-2.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-20.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-21.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-22.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-23.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-24.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-25.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-26.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-27.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-28.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-29.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-3.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-30.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-31.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-32.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-33.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-34.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-35.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-36.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-37.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-38.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-39.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-4.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-40.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-41.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-42.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-43.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-44.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-45.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-46.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-47.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-48.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-49.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-5.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-50.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-6.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-7.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-8.png +0 -0
- package/dist/assets/jdzcaptcha/streamline/light/icon-9.png +0 -0
- package/dist/public/css/jdzcaptcha.min.css +1 -0
- package/dist/public/js/jdzcaptcha.min.js +2 -0
- package/lib/iconsets/streamline/Icons by Streamline.txt +5 -0
- package/lib/iconsets/streamline/light/icon-1.png +0 -0
- package/lib/iconsets/streamline/light/icon-10.png +0 -0
- package/lib/iconsets/streamline/light/icon-11.png +0 -0
- package/lib/iconsets/streamline/light/icon-12.png +0 -0
- package/lib/iconsets/streamline/light/icon-13.png +0 -0
- package/lib/iconsets/streamline/light/icon-14.png +0 -0
- package/lib/iconsets/streamline/light/icon-15.png +0 -0
- package/lib/iconsets/streamline/light/icon-16.png +0 -0
- package/lib/iconsets/streamline/light/icon-17.png +0 -0
- package/lib/iconsets/streamline/light/icon-18.png +0 -0
- package/lib/iconsets/streamline/light/icon-19.png +0 -0
- package/lib/iconsets/streamline/light/icon-2.png +0 -0
- package/lib/iconsets/streamline/light/icon-20.png +0 -0
- package/lib/iconsets/streamline/light/icon-21.png +0 -0
- package/lib/iconsets/streamline/light/icon-22.png +0 -0
- package/lib/iconsets/streamline/light/icon-23.png +0 -0
- package/lib/iconsets/streamline/light/icon-24.png +0 -0
- package/lib/iconsets/streamline/light/icon-25.png +0 -0
- package/lib/iconsets/streamline/light/icon-26.png +0 -0
- package/lib/iconsets/streamline/light/icon-27.png +0 -0
- package/lib/iconsets/streamline/light/icon-28.png +0 -0
- package/lib/iconsets/streamline/light/icon-29.png +0 -0
- package/lib/iconsets/streamline/light/icon-3.png +0 -0
- package/lib/iconsets/streamline/light/icon-30.png +0 -0
- package/lib/iconsets/streamline/light/icon-31.png +0 -0
- package/lib/iconsets/streamline/light/icon-32.png +0 -0
- package/lib/iconsets/streamline/light/icon-33.png +0 -0
- package/lib/iconsets/streamline/light/icon-34.png +0 -0
- package/lib/iconsets/streamline/light/icon-35.png +0 -0
- package/lib/iconsets/streamline/light/icon-36.png +0 -0
- package/lib/iconsets/streamline/light/icon-37.png +0 -0
- package/lib/iconsets/streamline/light/icon-38.png +0 -0
- package/lib/iconsets/streamline/light/icon-39.png +0 -0
- package/lib/iconsets/streamline/light/icon-4.png +0 -0
- package/lib/iconsets/streamline/light/icon-40.png +0 -0
- package/lib/iconsets/streamline/light/icon-41.png +0 -0
- package/lib/iconsets/streamline/light/icon-42.png +0 -0
- package/lib/iconsets/streamline/light/icon-43.png +0 -0
- package/lib/iconsets/streamline/light/icon-44.png +0 -0
- package/lib/iconsets/streamline/light/icon-45.png +0 -0
- package/lib/iconsets/streamline/light/icon-46.png +0 -0
- package/lib/iconsets/streamline/light/icon-47.png +0 -0
- package/lib/iconsets/streamline/light/icon-48.png +0 -0
- package/lib/iconsets/streamline/light/icon-49.png +0 -0
- package/lib/iconsets/streamline/light/icon-5.png +0 -0
- package/lib/iconsets/streamline/light/icon-50.png +0 -0
- package/lib/iconsets/streamline/light/icon-6.png +0 -0
- package/lib/iconsets/streamline/light/icon-7.png +0 -0
- package/lib/iconsets/streamline/light/icon-8.png +0 -0
- package/lib/iconsets/streamline/light/icon-9.png +0 -0
- package/lib/index.less +5 -0
- package/lib/js/captcha.js +182 -0
- package/lib/js/constants.js +61 -0
- package/lib/js/fetch.js +51 -0
- package/lib/js/ui.js +117 -0
- package/lib/js/utils.js +159 -0
- package/lib/js/widget.js +624 -0
- package/lib/less/animations.less +45 -0
- package/lib/less/structure.less +259 -0
- package/lib/less/variables.less +2 -0
- package/lib/less/variants/dark.less +62 -0
- package/lib/less/variants/light.less +66 -0
- package/lib/placeholder.png +0 -0
- package/package.json +37 -0
package/lib/js/widget.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { CSS, defaults } from './constants.js';
|
|
2
|
+
import { Utils } from './utils.js';
|
|
3
|
+
import { UI } from './ui.js';
|
|
4
|
+
import Fetch from './fetch.js';
|
|
5
|
+
|
|
6
|
+
export class Widget {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new Widget instance.
|
|
9
|
+
* @param {HTMLElement} $element The DOM element to generate the widget into.
|
|
10
|
+
* @param {Object} options An object containing the configuration options for the widget.
|
|
11
|
+
*/
|
|
12
|
+
constructor($element, options) {
|
|
13
|
+
// ignore the element if it is not a valid DOM element
|
|
14
|
+
if (!$element) {
|
|
15
|
+
Utils.warn('Element is not a valid DOM element.');
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Ensure the `path` option is set
|
|
20
|
+
if (!options.path) {
|
|
21
|
+
this.error('The option "path" has not been set.');
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ignore the element if it is not a valid DOM element
|
|
26
|
+
if ($element.dataset.jdzcId) {
|
|
27
|
+
Utils.warn('The widget is already initialized.');
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.id = this.generateCaptchaId();
|
|
32
|
+
this.$element = $element;
|
|
33
|
+
this.$element.dataset.jdzcId = this.id;
|
|
34
|
+
this.$iconHolder = null;
|
|
35
|
+
this.token = null;
|
|
36
|
+
this.startedInitialization = false;
|
|
37
|
+
this.invalidateTimeoutId = null;
|
|
38
|
+
this.captchaImageWidth = 0;
|
|
39
|
+
this.generated = false; // Tracks if the captcha is fully generated
|
|
40
|
+
this.generatedInTime = 0;
|
|
41
|
+
this.hovering = false; // Tracks if the user is hovering over the selection area
|
|
42
|
+
this.submitting = false; // Tracks if the captcha is currently submitting
|
|
43
|
+
this.options = options;
|
|
44
|
+
|
|
45
|
+
this.generate();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generates the widget and sets up event listeners.
|
|
50
|
+
*/
|
|
51
|
+
generate() {
|
|
52
|
+
if (this.generated) {
|
|
53
|
+
Utils.warn('The widget ' + this.id + ' has already been generated.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get the CSRF token from the closest form, if available
|
|
58
|
+
const $form = this.$element.closest('form');
|
|
59
|
+
if ($form) {
|
|
60
|
+
const $tokenInput = $form.querySelector(`input[name = "jdzc[${this.options.fields.token}]"]`);
|
|
61
|
+
this.token = $tokenInput ? $tokenInput.value : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Throw an exception if the token is not found
|
|
65
|
+
if (!this.token) {
|
|
66
|
+
Utils.error('CSRF token is missing or invalid for widget[' + this.id + ']. Ensure the form contains a valid input field for the token.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.$element.setAttribute('data-theme', this.options.theme);
|
|
71
|
+
this.$element.setAttribute('data-series', this.options.series);
|
|
72
|
+
this.$element.classList.add(`jdzc-theme-${this.options.theme}`);
|
|
73
|
+
|
|
74
|
+
// Apply the custom font family, if set
|
|
75
|
+
if (this.options.fontFamily) {
|
|
76
|
+
this.$element.style.fontFamily = this.options.fontFamily;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// If not initialized yet, show the 'initial' captcha holder and wait for click
|
|
80
|
+
if (!this.startedInitialization && this.options.security.enableInitialMessage) {
|
|
81
|
+
this.startedInitialization = true;
|
|
82
|
+
|
|
83
|
+
this.$element.classList.add(CSS.init);
|
|
84
|
+
this.$element.classList.remove(CSS.error, CSS.success);
|
|
85
|
+
this.$element.innerHTML = UI.buildCaptchaInitialHolder(this.options);
|
|
86
|
+
|
|
87
|
+
// Wait for user click to start loading
|
|
88
|
+
this.$element.addEventListener('click', () => {
|
|
89
|
+
this.$element.classList.remove(CSS.init);
|
|
90
|
+
this.generate();
|
|
91
|
+
}, { once: true });
|
|
92
|
+
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build the captcha if it hasn't been built yet
|
|
97
|
+
if (!this.generated) {
|
|
98
|
+
this.$element.innerHTML = UI.buildCaptchaHolder(this.options, this.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Assign the icon holder
|
|
102
|
+
this.$iconHolder = this.$element.querySelector(`.${CSS.boxB}`);
|
|
103
|
+
|
|
104
|
+
// Add the loading spinner
|
|
105
|
+
UI.addLoadingSpinner(this.$iconHolder);
|
|
106
|
+
|
|
107
|
+
// If the loadingAnimationDelay has been set and is not 0, add the loading delay
|
|
108
|
+
if (this.options.security.loadingAnimationDelay && this.options.security.loadingAnimationDelay > 0 && !this.options.security.enableInitialMessage) {
|
|
109
|
+
setTimeout(() => this.load(), this.options.security.loadingAnimationDelay);
|
|
110
|
+
} else {
|
|
111
|
+
this.load();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Register event listener for the selection area
|
|
115
|
+
const $selectionArea = this.$element.querySelector(`.${CSS.selection}`);
|
|
116
|
+
if ($selectionArea) {
|
|
117
|
+
$selectionArea.addEventListener('click', (event) => {
|
|
118
|
+
const rect = $selectionArea.getBoundingClientRect();
|
|
119
|
+
const xPos = event.clientX - rect.left;
|
|
120
|
+
const yPos = event.clientY - rect.top;
|
|
121
|
+
|
|
122
|
+
// Call submitIconSelection with the calculated coordinates
|
|
123
|
+
this.submitIconSelection(xPos, yPos);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.generated = true; // Set generated to true after successful generation
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Loads the captcha data for the widget.
|
|
132
|
+
*/
|
|
133
|
+
load() {
|
|
134
|
+
if (this.generated) {
|
|
135
|
+
return; // Prevent loading if already generated
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const requestPayload = Utils.createPayload({
|
|
139
|
+
i: this.id,
|
|
140
|
+
a: 1,
|
|
141
|
+
t: (this.$element.getAttribute('data-series') || 'streamline') + '/' + (this.$element.getAttribute('data-theme') || 'light'),
|
|
142
|
+
tk: this.token,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
Fetch({
|
|
146
|
+
url: this.options.path,
|
|
147
|
+
type: 'POST',
|
|
148
|
+
headers: this.createHeaders(this.token),
|
|
149
|
+
data: { payload: requestPayload },
|
|
150
|
+
success: (data) => {
|
|
151
|
+
if (data && typeof data === 'string' && Utils.isBase64(data)) {
|
|
152
|
+
const result = JSON.parse(atob(data));
|
|
153
|
+
|
|
154
|
+
if (result.error) {
|
|
155
|
+
UI.processCaptchaRequestError(result.error, result.data);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create the Base64 payload.
|
|
160
|
+
const imageRequestPayload = Utils.createPayload({ i: this.id, tk: this.token });
|
|
161
|
+
const urlParamSeparator = this.options.path.indexOf('?') > -1 ? '&' : '?';
|
|
162
|
+
|
|
163
|
+
// Load the captcha image.
|
|
164
|
+
const $iconsHolder = this.$iconHolder.querySelector(`.${CSS.icons}`);
|
|
165
|
+
$iconsHolder.style.backgroundImage = `url(${this.options.path}${urlParamSeparator}payload=${imageRequestPayload})`;
|
|
166
|
+
|
|
167
|
+
UI.removeLoadingSpinnerOnImageLoad($iconsHolder, () => UI.removeLoadingSpinner(this.$iconHolder));
|
|
168
|
+
|
|
169
|
+
// Add the selection area to the captcha holder.
|
|
170
|
+
$iconsHolder.parentNode.insertAdjacentHTML('beforeend', `<div class="${CSS.selection}"><i></i></div>`);
|
|
171
|
+
const selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
|
|
172
|
+
|
|
173
|
+
// Register the events.
|
|
174
|
+
this.registerSelectionEvents();
|
|
175
|
+
|
|
176
|
+
// Trigger the 'init' event if not already generated.
|
|
177
|
+
if (!this.generated) {
|
|
178
|
+
Utils.trigger(this.$element, 'jdzc.init', { captchaId: this.id, options: this.options });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Determine the width of the image.
|
|
182
|
+
const modalSelection = this.$iconHolder.querySelector(`.${CSS.selection}`);
|
|
183
|
+
this.captchaImageWidth = Utils.width(modalSelection);
|
|
184
|
+
|
|
185
|
+
// Set the building timestamp.
|
|
186
|
+
this.generatedInTime = new Date();
|
|
187
|
+
this.generated = true;
|
|
188
|
+
|
|
189
|
+
// Start the invalidation timer and save the timer identifier.
|
|
190
|
+
this.invalidateTimeoutId = setTimeout(() => this.invalidateSession(true), this.options.security.invalidateTime);
|
|
191
|
+
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.setCaptchaError('The JdzCaptcha could not be loaded.', 'Invalid data was returned by the captcha back-end service. Make sure JdzCaptcha is installed/configured properly.');
|
|
196
|
+
},
|
|
197
|
+
error: () => this.showIncorrectIconMessage(
|
|
198
|
+
this.options.messages.incorrect.title, // Top message
|
|
199
|
+
this.options.messages.incorrect.subtitle, // Bottom message
|
|
200
|
+
true // Reset the captcha
|
|
201
|
+
),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.generated = true; // Set generated to true after successful loading
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resets the widget.
|
|
209
|
+
*/
|
|
210
|
+
reset() {
|
|
211
|
+
Utils.clearInvalidationTimeout(this.invalidateTimeoutId);
|
|
212
|
+
this.startedInitialization = false;
|
|
213
|
+
this.generated = false;
|
|
214
|
+
Utils.trigger(this.$element, 'jdzc.reset', { captchaId: this.id });
|
|
215
|
+
this.generate();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Invalidates the captcha session.
|
|
220
|
+
* @param {boolean} invalidateServer Whether to invalidate the session on the server side.
|
|
221
|
+
*/
|
|
222
|
+
invalidateSession(invalidateServer = true) {
|
|
223
|
+
// Reset the captcha state
|
|
224
|
+
this.generated = false;
|
|
225
|
+
this.startedInitialization = false;
|
|
226
|
+
|
|
227
|
+
// If server-side invalidation is required
|
|
228
|
+
if (invalidateServer) {
|
|
229
|
+
const payload = Utils.createPayload({ i: this.id, a: 3, tk: this.token });
|
|
230
|
+
|
|
231
|
+
Fetch({
|
|
232
|
+
url: this.options.path,
|
|
233
|
+
type: 'POST',
|
|
234
|
+
headers: this.createHeaders(this.token),
|
|
235
|
+
data: { payload },
|
|
236
|
+
success: () => {
|
|
237
|
+
// Trigger the 'invalidated' event
|
|
238
|
+
Utils.trigger(this.$element, 'jdzc.invalidated', { captchaId: this.id });
|
|
239
|
+
|
|
240
|
+
// Reset the captcha holder
|
|
241
|
+
this.resetCaptchaHolder();
|
|
242
|
+
},
|
|
243
|
+
error: () => {
|
|
244
|
+
// Handle error during server-side invalidation
|
|
245
|
+
this.setCaptchaError('The JdzCaptcha could not be reset.', 'Invalid data was returned by the captcha back-end service. Make sure JdzCaptcha is installed/configured properly.');
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
} else {
|
|
249
|
+
// Reset the captcha holder directly if no server-side invalidation is required
|
|
250
|
+
this.resetCaptchaHolder();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Submits the icon selection made by the user to the server for validation.
|
|
256
|
+
* @param {number} xPos The clicked X position.
|
|
257
|
+
* @param {number} yPos The clicked Y position.
|
|
258
|
+
*/
|
|
259
|
+
submitIconSelection(xPos, yPos) {
|
|
260
|
+
if (this.submitting) {
|
|
261
|
+
return; // Prevent duplicate submissions
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (xPos !== undefined && yPos !== undefined) {
|
|
265
|
+
this.submitting = true; // Set submitting to true
|
|
266
|
+
|
|
267
|
+
// Stop the reset timeout.
|
|
268
|
+
Utils.clearInvalidationTimeout(this.invalidateTimeoutId);
|
|
269
|
+
|
|
270
|
+
// Round the clicked position.
|
|
271
|
+
xPos = Math.round(xPos);
|
|
272
|
+
yPos = Math.round(yPos);
|
|
273
|
+
|
|
274
|
+
// Update the form fields with the captcha data.
|
|
275
|
+
const $selectionField = this.$element.querySelector(`input[name = "jdzc[${this.options.fields.selection}]"]`);
|
|
276
|
+
const $idField = this.$element.querySelector(`input[name = "jdzc[${this.options.fields.id}]"]`);
|
|
277
|
+
if ($selectionField) {
|
|
278
|
+
$selectionField.setAttribute('value', [xPos, yPos, this.captchaImageWidth].join(','));
|
|
279
|
+
}
|
|
280
|
+
if ($idField) {
|
|
281
|
+
$idField.setAttribute('value', this.id);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Hide the mouse cursor.
|
|
285
|
+
const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
|
|
286
|
+
if ($selectionCursor) {
|
|
287
|
+
$selectionCursor.style.display = 'none';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Create the Base64 payload.
|
|
291
|
+
const requestPayload = Utils.createPayload({
|
|
292
|
+
i: this.id,
|
|
293
|
+
x: xPos,
|
|
294
|
+
y: yPos,
|
|
295
|
+
w: this.captchaImageWidth,
|
|
296
|
+
a: 2,
|
|
297
|
+
tk: this.token,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Perform the request.
|
|
301
|
+
Fetch({
|
|
302
|
+
url: this.options.path,
|
|
303
|
+
type: 'POST',
|
|
304
|
+
headers: this.createHeaders(this.token),
|
|
305
|
+
data: { payload: requestPayload },
|
|
306
|
+
success: (data) => {
|
|
307
|
+
this.submitting = false; // Reset submitting to false
|
|
308
|
+
if (data.success === true) {
|
|
309
|
+
this.showCompletionMessage();
|
|
310
|
+
} else {
|
|
311
|
+
this.showIncorrectIconMessage();
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
error: () => {
|
|
315
|
+
this.submitting = false; // Reset submitting to false
|
|
316
|
+
this.setCaptchaError('The JdzCaptcha selection could not be submitted.', 'Invalid data was returned by the captcha back-end service. Make sure JdzCaptcha is installed/configured properly.');
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Displays the success message when the correct icon is selected.
|
|
324
|
+
*/
|
|
325
|
+
showCompletionMessage() {
|
|
326
|
+
// Add the success state and remove the error state
|
|
327
|
+
this.$element.classList.add(CSS.success);
|
|
328
|
+
this.$element.classList.remove(CSS.error);
|
|
329
|
+
|
|
330
|
+
// Display the success message
|
|
331
|
+
this.$iconHolder.innerHTML = UI.buildValidSelectionMessage(this.options);
|
|
332
|
+
|
|
333
|
+
// Unregister the selection events
|
|
334
|
+
this.unregisterSelectionEvents();
|
|
335
|
+
|
|
336
|
+
// Trigger the 'success' event
|
|
337
|
+
Utils.trigger(this.$element, 'jdzc.success', { captchaId: this.id });
|
|
338
|
+
|
|
339
|
+
// Reset the captcha after a delay
|
|
340
|
+
setTimeout(() => this.reset(), this.options.security.selectionResetDelay);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Displays the error message when the incorrect icon is selected.
|
|
345
|
+
*/
|
|
346
|
+
showIncorrectIconMessage() {
|
|
347
|
+
this.$element.classList.add(CSS.error);
|
|
348
|
+
this.$element.classList.remove(CSS.success);
|
|
349
|
+
this.$iconHolder.innerHTML = UI.buildInvalidSelectionMessage(this.options);
|
|
350
|
+
|
|
351
|
+
// Trigger the 'error' event.
|
|
352
|
+
Utils.trigger(this.$element, 'jdzc.error', { captchaId: this.id });
|
|
353
|
+
|
|
354
|
+
// Reset the captcha after a delay.
|
|
355
|
+
setTimeout(() => this.reset(), this.options.security.selectionResetDelay);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Logs a serious error that prevents the plugin from initializing and updates the captcha state.
|
|
360
|
+
* @param {string} displayError The error message to display in the captcha holder element.
|
|
361
|
+
* @param {string} [consoleError] The error message to display in the developer console. If not provided, `displayError` will be used.
|
|
362
|
+
* @param {boolean} triggerEvent Whether to trigger the custom 'error' event.
|
|
363
|
+
*/
|
|
364
|
+
setCaptchaError(displayError, consoleError = '', triggerEvent = true) {
|
|
365
|
+
const DEBUG_MODE = window.JdzCaptcha && window.JdzCaptcha.debugMode;
|
|
366
|
+
|
|
367
|
+
// Determine the error messages to display.
|
|
368
|
+
const topMessage = DEBUG_MODE ? 'JdzCaptcha error' : this.messages.incorrect.title;
|
|
369
|
+
const bottomMessage = DEBUG_MODE ? displayError : this.messages.incorrect.subtitle;
|
|
370
|
+
const errorReset = !DEBUG_MODE;
|
|
371
|
+
|
|
372
|
+
// Display the error message in the captcha holder.
|
|
373
|
+
this.showIncorrectIconMessage(topMessage, bottomMessage, errorReset);
|
|
374
|
+
|
|
375
|
+
// Log the error to the console.
|
|
376
|
+
Utils.error(consoleError || displayError);
|
|
377
|
+
|
|
378
|
+
// Trigger the custom 'error' event if required.
|
|
379
|
+
if (triggerEvent) {
|
|
380
|
+
Utils.trigger(this.$element, 'jdzc.error', { captchaId: this.id });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Processes the error data which was received from the server while requesting the captcha data. Actions
|
|
386
|
+
* might be performed based on the given error code or error data.
|
|
387
|
+
* @param code The error code.
|
|
388
|
+
* @param data The payload of the error.
|
|
389
|
+
*/
|
|
390
|
+
processCaptchaRequestError(code, data) {
|
|
391
|
+
code = parseInt(code);
|
|
392
|
+
|
|
393
|
+
switch (code) {
|
|
394
|
+
case 1: // Too many incorrect selections, timeout.
|
|
395
|
+
this.showIncorrectIconMessage(this.options.messages.timeout.title, this.options.messages.timeout.subtitle, false);
|
|
396
|
+
|
|
397
|
+
// Remove the header from the captcha.
|
|
398
|
+
const captchaHeader = this.$element.querySelector(`.${CSS.boxH}`);
|
|
399
|
+
captchaHeader.parentNode.removeChild(captchaHeader);
|
|
400
|
+
|
|
401
|
+
// Trigger: timeout
|
|
402
|
+
Utils.trigger(this.$element, 'jdzc.timeout', { captchaId: this.id });
|
|
403
|
+
|
|
404
|
+
// Reset the captcha to the init holder.
|
|
405
|
+
setTimeout(() => this.invalidateSession(false), data);
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 2: // No CSRF token found while validating.
|
|
409
|
+
this.setCaptchaError('The captcha token is missing or is incorrect.', 'A server request was made without including a captcha token, however this option is enabled.');
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
default: // Any other error.
|
|
413
|
+
this.setCaptchaError('An unexpected error occurred.', 'An unexpected error occurred while JdzCaptcha performed an action.');
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Changes the captcha state to the 'error' state.
|
|
420
|
+
* @param {string} [topMessage] The title message of the error state.
|
|
421
|
+
* @param {string} [bottomMessage] The subtitle message of the error state.
|
|
422
|
+
* @param {boolean} [reset=true] Whether the captcha should reinitialize automatically after some time.
|
|
423
|
+
*/
|
|
424
|
+
showIncorrectIconMessage(topMessage = null, bottomMessage = null, reset = true) {
|
|
425
|
+
topMessage = topMessage || this.options.messages.incorrect.title;
|
|
426
|
+
bottomMessage = bottomMessage || this.options.messages.incorrect.subtitle;
|
|
427
|
+
|
|
428
|
+
// Remove opacity styles
|
|
429
|
+
this.$iconHolder.classList.remove(CSS.opacity);
|
|
430
|
+
this.$element.classList.remove(CSS.opacity);
|
|
431
|
+
|
|
432
|
+
// Unregister the selection events
|
|
433
|
+
this.unregisterSelectionEvents();
|
|
434
|
+
|
|
435
|
+
// Add the error state and display the error message
|
|
436
|
+
this.$element.classList.add(CSS.error);
|
|
437
|
+
this.$iconHolder.innerHTML = UI.buildErrorMessage(topMessage, bottomMessage);
|
|
438
|
+
|
|
439
|
+
// Mark the captcha as 'not submitting'
|
|
440
|
+
this.submitting = false;
|
|
441
|
+
|
|
442
|
+
// Trigger the 'error' event
|
|
443
|
+
Utils.trigger(this.$element, 'jdzc.error', { captchaId: this.id });
|
|
444
|
+
|
|
445
|
+
// Handle timeout or reset the captcha
|
|
446
|
+
if (reset) {
|
|
447
|
+
setTimeout(() => this.reset(), this.options.security.selectionResetDelay);
|
|
448
|
+
} else {
|
|
449
|
+
// Trigger a timeout event if reset is disabled
|
|
450
|
+
Utils.trigger(this.$element, 'jdzc.timeout', { captchaId: this.id });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Resets the state of the captcha holder element.
|
|
456
|
+
* The error state will be removed, hidden input fields will be cleared, and the captcha will be reinitialized.
|
|
457
|
+
*/
|
|
458
|
+
resetCaptchaHolder() {
|
|
459
|
+
// Remove the error state
|
|
460
|
+
this.$element.classList.remove(CSS.error);
|
|
461
|
+
|
|
462
|
+
// Clear the selection input field
|
|
463
|
+
const $selectionField = this.$element.querySelector(`input[name = "jdzc[${this.options.fields.selection}]"]`);
|
|
464
|
+
if ($selectionField) {
|
|
465
|
+
$selectionField.setAttribute('value', null);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Reset the captcha body
|
|
469
|
+
Utils.empty(this.$iconHolder);
|
|
470
|
+
this.$iconHolder.insertAdjacentHTML('beforeend', `<div class="${CSS.icons}"></div>`);
|
|
471
|
+
|
|
472
|
+
// Reload the captcha
|
|
473
|
+
this.generate();
|
|
474
|
+
|
|
475
|
+
// Trigger the 'refreshed' event
|
|
476
|
+
Utils.trigger(this.$element, 'jdzc.refreshed', { captchaId: this.id });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Registers events linked to the captcha selection area element.
|
|
481
|
+
*/
|
|
482
|
+
registerSelectionEvents() {
|
|
483
|
+
const $captchaSelection = this.$element.querySelector(`.${CSS.selection}`);
|
|
484
|
+
|
|
485
|
+
// Ensure the element and its cached listeners do not exist.
|
|
486
|
+
if (!$captchaSelection || $captchaSelection._jdzc_listeners) return;
|
|
487
|
+
|
|
488
|
+
const handlers = {
|
|
489
|
+
click: this.mouseClickEvent.bind(this),
|
|
490
|
+
mousemove: this.mouseMoveEvent.bind(this),
|
|
491
|
+
mouseenter: this.mouseEnterEvent.bind(this),
|
|
492
|
+
mouseleave: this.mouseLeaveEvent.bind(this),
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Cache the listeners for later removal.
|
|
496
|
+
$captchaSelection._jdzc_listeners = handlers;
|
|
497
|
+
|
|
498
|
+
// Register the events.
|
|
499
|
+
Object.entries(handlers).forEach(([event, handler]) => {
|
|
500
|
+
$captchaSelection.addEventListener(event, handler);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Unregisters all event listeners linked to the captcha selection area element.
|
|
506
|
+
*/
|
|
507
|
+
unregisterSelectionEvents() {
|
|
508
|
+
const $captchaSelection = this.$element.querySelector(`.${CSS.selection}`);
|
|
509
|
+
|
|
510
|
+
// Ensure the element and its cached listeners exist.
|
|
511
|
+
if (!$captchaSelection || !$captchaSelection._jdzc_listeners) return;
|
|
512
|
+
|
|
513
|
+
// Unregister each cached event listener.
|
|
514
|
+
Object.entries($captchaSelection._jdzc_listeners).forEach(([event, handler]) => {
|
|
515
|
+
$captchaSelection.removeEventListener(event, handler);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Clear the cached listeners to free memory.
|
|
519
|
+
delete $captchaSelection._jdzc_listeners;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Moves the custom cursor to the current location of the actual cursor.
|
|
524
|
+
* @param {MouseEvent} event The mouse move event.
|
|
525
|
+
*/
|
|
526
|
+
moveCustomCursor(event) {
|
|
527
|
+
if (!event.currentTarget) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Calculate the clicked X and Y position.
|
|
532
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
533
|
+
const xPos = Math.round(event.clientX - rect.left);
|
|
534
|
+
const yPos = Math.round(event.clientY - rect.top);
|
|
535
|
+
|
|
536
|
+
// Apply the style position to the cursor.
|
|
537
|
+
const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
|
|
538
|
+
if ($selectionCursor) {
|
|
539
|
+
$selectionCursor.style.left = `${xPos - 8}px`;
|
|
540
|
+
$selectionCursor.style.top = `${yPos - 7}px`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Handles the user's click on the captcha selection area.
|
|
546
|
+
* @param {MouseEvent} event The mouse click event.
|
|
547
|
+
*/
|
|
548
|
+
mouseClickEvent(event) {
|
|
549
|
+
if (!this.generated || this.submitting) {
|
|
550
|
+
return; // Prevent clicking if the captcha is not ready or already submitting
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (new Date() - this.generatedInTime <= this.options.security.clickDelay) {
|
|
554
|
+
return; // Only allow a user to click after a set click delay
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
558
|
+
const xPos = event.clientX - rect.left;
|
|
559
|
+
const yPos = event.clientY - rect.top;
|
|
560
|
+
|
|
561
|
+
if (!xPos || !yPos) {
|
|
562
|
+
return; // Invalid click
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.submitIconSelection(xPos, yPos);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Updates the position of the custom cursor as the user moves the mouse.
|
|
570
|
+
* @param {MouseEvent} event The mouse move event.
|
|
571
|
+
*/
|
|
572
|
+
mouseMoveEvent(event) {
|
|
573
|
+
if (!this.hovering || this.submitting || !this.generated) {
|
|
574
|
+
return; // Prevent cursor movement if not hovering, submitting, or generated
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.moveCustomCursor(event);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Handles the mouse entering the selection area.
|
|
582
|
+
* @param {MouseEvent} event The mouse enter event.
|
|
583
|
+
*/
|
|
584
|
+
mouseEnterEvent(event) {
|
|
585
|
+
const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
|
|
586
|
+
if ($selectionCursor) {
|
|
587
|
+
$selectionCursor.style.display = 'inline'; // Show the cursor
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.hovering = true; // Set hovering to true
|
|
591
|
+
this.moveCustomCursor(event); // Update cursor position
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Handles the mouse leaving the selection area.
|
|
596
|
+
*/
|
|
597
|
+
mouseLeaveEvent() {
|
|
598
|
+
const $selectionCursor = this.$iconHolder.querySelector(`.${CSS.selection} > i`);
|
|
599
|
+
if ($selectionCursor) {
|
|
600
|
+
$selectionCursor.style.display = 'none'; // Hide the cursor
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.hovering = false; // Set hovering to false
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Creates the custom header object which should be included in every AJAX request.
|
|
608
|
+
* @param {string} [token] The captcha session token, possibly empty.
|
|
609
|
+
* @returns {Object} The header object.
|
|
610
|
+
*/
|
|
611
|
+
createHeaders(token) {
|
|
612
|
+
return token ? { 'X-JdzCaptcha-Token': token } : {};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Generates a random captcha identifier.
|
|
618
|
+
* @returns {number} The widget identifier.
|
|
619
|
+
*/
|
|
620
|
+
generateCaptchaId() {
|
|
621
|
+
const maxNumber = 10 ** 13 - 1; // Maximum 13-digit number
|
|
622
|
+
return Math.floor(Math.random() * maxNumber);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
@-webkit-keyframes jdzc-breath {
|
|
2
|
+
0% {
|
|
3
|
+
transform: scale(1) translateZ(0);
|
|
4
|
+
border-color: @jdz-color;
|
|
5
|
+
}
|
|
6
|
+
25% {
|
|
7
|
+
transform: scale(0.8) translateZ(0);
|
|
8
|
+
border-color: @jdz-color-light;
|
|
9
|
+
}
|
|
10
|
+
50% {
|
|
11
|
+
transform: scale(1) translateZ(0);
|
|
12
|
+
border-color: @jdz-color;
|
|
13
|
+
}
|
|
14
|
+
75% {
|
|
15
|
+
transform: scale(0.8) translateZ(0);
|
|
16
|
+
border-color: @jdz-color-light;
|
|
17
|
+
}
|
|
18
|
+
100% {
|
|
19
|
+
transform: scale(1) translateZ(0);
|
|
20
|
+
border-color: @jdz-color;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@keyframes jdzc-breath {
|
|
25
|
+
0% {
|
|
26
|
+
transform: scale(1) translateZ(0);
|
|
27
|
+
border-color: @jdz-color;
|
|
28
|
+
}
|
|
29
|
+
25% {
|
|
30
|
+
transform: scale(0.8) translateZ(0);
|
|
31
|
+
border-color: @jdz-color-light;
|
|
32
|
+
}
|
|
33
|
+
50% {
|
|
34
|
+
transform: scale(1) translateZ(0);
|
|
35
|
+
border-color: @jdz-color;
|
|
36
|
+
}
|
|
37
|
+
75% {
|
|
38
|
+
transform: scale(0.8) translateZ(0);
|
|
39
|
+
border-color: @jdz-color-light;
|
|
40
|
+
}
|
|
41
|
+
100% {
|
|
42
|
+
transform: scale(1) translateZ(0);
|
|
43
|
+
border-color: @jdz-color;
|
|
44
|
+
}
|
|
45
|
+
}
|