PageSourceSearch

https://subscribe.purlmagazine.com/js/jquery.cardswipe.js

js purlmagazine.com collected 2026-10-10 00:43:15 UTC 15,450 bytes, 594 lines download raw bytes

1/*! jQuery.CardSwipe Magnetic Stripe Card Reader - v1.3.0 - 2018-03-08
2* https://github.com/CarlRaymond/jquery.cardswipe
3* Copyright (c) 2018 Carl J. Raymond; Licensed MIT */
4// A jQuery plugin to detect magnetic card swipes.  Requires a card reader that simulates a keyboard.
5// This expects a card that encodes data on track 1, though it also reads tracks 2 and 3.  Most cards
6// use track 1.  This won't recognize cards that don't use track 1, or work with a reader that
7// doesn't read track 1.
8//
9// See http://en.wikipedia.org/wiki/Magnetic_card to understand the format of the data on a card.
10//
11// Uses pattern at https://github.com/umdjs/umd/blob/master/jqueryPlugin.js to declare
12// the plugin so that it works with or without an AMD-compatible module loader, like RequireJS.
13(function (factory) {
14    if (typeof define === 'function' && define.amd) {
15        // AMD. Register as an anonymous module.
16        define(['jquery'], factory);
17    } else {
18        // Browser globals
19        factory(jQuery);
20    }
21}(function ($) {
22
23	// The plugin proper.  Dispatches methods using the usual jQuery pattern.
24	var plugin = function (method) {
25		// Method calling logic. If named method exists, execute it with passed arguments
26		if (methods[method]) {
27			return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
28		}
29			// If no argument, or an object passed, invoke init method.
30		else if (typeof method === 'object' || !method) {
31			return methods.init.apply(this, arguments);
32		}
33		else {
34			throw 'Method ' + method + ' does not exist on jQuery.cardswipe';
35		}
36	};
37
38	// Built-in parsers. These include simplistic credit card parsers that
39	// recognize various card issuers based on patterns of the account number.
40	// There is no guarantee these are correct or complete; they are based
41	// on information from Wikipedia.
42	// Account numbers are validated by the Luhn checksum algorithm.
43	var builtinParsers = {
44
45		// Generic parser. Separates raw data into up to three lines.
46		generic: function (rawData) {
47				var pattern = new RegExp("^(%[^%;\\?]+\\?)?(;[0-9\\:<>\\=]+\\?)?([+;][0-9\\:<>\\=]+\\?)?");
48
49				var match = pattern.exec(rawData);
50				if (!match) return null;
51
52				// Extract the three lines
53				var cardData = {
54					type: "generic",
55					line1: match[1] ? match[1].slice(1, -1) : "",
56					line2: match[2] ? match[2].slice(1, -1) : "",
57					line3: match[3] ? match[3].slice(1, -1) : ""
58				};
59
60				return cardData;
61			},
62
63
64		// Visa card parser.
65		visa: function (rawData) {
66			// Visa issuer number begins with 4 and may vary from 13 to 19 total digits. 16 digits is most common.
67			var pattern = new RegExp("^%B(4[0-9]{12,18})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})");
68
69			var match = pattern.exec(rawData);
70			if (!match) return null;
71
72			var account = match[1];
73			if (!luhnChecksum(account))
74				return null;
75
76			var cardData = {
77				type: "visa",
78				account: account,
79				lastName: match[2].trim(),
80				firstName: match[3].trim(),
81				honorific: match[4] ? match[4].trim().slice(1) : "",
82				expYear: match[5],
83				expMonth: match[6]
84			};
85
86			return cardData;
87		},
88
89		// MasterCard parser.
90		mastercard: function (rawData) {
91			// MasterCard starts with 51-55, and is 16 digits long.
92			var pattern = new RegExp("^%B(5[1-5][0-9]{14})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})");
93
94			var match = pattern.exec(rawData);
95			if (!match) return null;
96
97			var account = match[1];
98			if (!luhnChecksum(account))
99				return null;
100
101			var cardData = {
102				type: "mastercard",
103				account: account,
104				lastName: match[2],
105				firstName: match[3],
106				honorific: match[4] ? match[4].trim().slice(1) : "",
107 				expYear: match[5],
108				expMonth: match[6]
109			};
110
111			return cardData;
112		},
113
114		// Discover parser.
115		discover: function (rawData) {
116			// discover starts with 6, and is 16 digits long.
117			var pattern = new RegExp("^%B(6[0-9]{15})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})");
118
119			var match = pattern.exec(rawData);
120			if (!match) return null;
121
122			var account = match[1];
123			if (!luhnChecksum(account))
124				return null;
125
126			var cardData = {
127				type: "discover",
128				account: account,
129				lastName: match[2],
130				firstName: match[3],
131				honorific: match[4] ? match[4].trim().slice(1) : "",
132				expYear: match[5],
133				expMonth: match[6]
134			};
135
136			return cardData;
137		},
138
139		// American Express parser
140		amex: function (rawData) {
141			// American Express starts with 34 or 37, and is 15 digits long.
142			var pattern = new RegExp("^%B(3[4|7][0-9]{13})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})");
143
144			var match = pattern.exec(rawData);
145			if (!match) return null;
146
147			var account = match[1];
148			if (!luhnChecksum(account))
149				return null;
150
151			var cardData = {
152				type: "amex",
153				account: account,
154				lastName: match[2],
155				firstName: match[3],
156				honorific: match[4] ? match[4].trim().slice(1) : "",
157				expYear: match[5],
158				expMonth: match[6]
159			};
160
161			return cardData;
162		}
163	};
164
165
166
167
168	// State definitions:
169	var states = { IDLE: 0, PENDING1: 1, PENDING2: 2, READING: 3, DISCARD: 4, PREFIX: 5 };
170
171	// State names used when debugging.
172	var stateNames = { 0: 'IDLE', 1: 'PENDING1', 2: 'PENDING2', 3: 'READING', 4: 'DISCARD', 5: 'PREFIX' };
173
174	// Holds current state. Update only through state function.
175	var currentState = states.IDLE;
176
177	// Gets or sets the current state.
178	var state = function() {
179
180		if (arguments.length === 0) {
181			return currentState;
182		}
183
184		// Set new state.
185		var newState = arguments[0];
186		if (newState == state)
187			return;
188
189		if (settings.debug) { console.log("%s -> %s", stateNames[currentState], stateNames[newState]); }
190
191		// Raise events when entering and leaving the READING state
192		if (newState == states.READING)
193			$eventSource.trigger("scanstart.cardswipe");
194
195		if (currentState == states.READING)
196			$eventSource.trigger("scanend.cardswipe");
197
198		currentState = newState;
199	};
200
201	// Array holding scanned characters
202	var scanbuffer;
203
204	// Interdigit timer
205	var timerHandle = 0;
206
207	// Keypress listener
208	var listener = function (e) {
209		if (settings.debug) { console.log(e.which + ': ' + String.fromCharCode(e.which));}
210		switch (state()) {
211
212			// IDLE: Look for prfix characters or line 1 or line 2 start
213			// characters, and jump to PENDING1 or PENDING2.
214		 	case states.IDLE:
215				// Look for prefix characters, and jump to PREFIX.
216				if (isInPrefixCodes(e.which)) {
217					state(states.PREFIX);
218					e.preventDefault();
219					e.stopPropagation();
220					startTimer();
221				}
222
223				// Cards with (and readers reading) line 1:
224				// look for '%', and jump to PENDING1.
225				if (e.which == 37) {
226					state(states.PENDING1);
227					scanbuffer = [];
228					processCode(e.which);
229					e.preventDefault();
230					e.stopPropagation();
231					startTimer();
232				}
233
234				// Cards without (or readers ignoring) line 1:
235				// look for ';', and jump to PENDING_LINE
236				if (e.which == 59) {
237					state(states.PENDING2);
238					scanbuffer = [];
239					processCode(e.which);
240					e.preventDefault();
241					e.stopPropagation();
242					startTimer();
243				}
244
245				break;
246
247			// PENDING1: Look for A-Z then jump to READING.
248			// Otherwise, pass the keypress through, reset and jump to IDLE.
249			case states.PENDING1:
250				// Look for format code character, A-Z. Almost always B for cards
251				// used by the general public. Some reader / OS combinations
252				// will issue lowercase characters when the caps lock key is on.
253				if ((e.which >= 65 && e.which <= 90) || (e.which >= 97 && e.which <= 122)) {
254					state(states.READING);
255
256					// Leaving focus on a form element wreaks browser-dependent
257					// havoc because of keyup and keydown events.  This is a
258					// cross-browser way to prevent trouble.
259					$("input").blur();
260
261					processCode(e.which);
262					e.preventDefault();
263					e.stopPropagation();
264					startTimer();
265				}
266				else {
267					clearTimer();
268					scanbuffer = null;
269					state(states.IDLE);
270				}
271				break;
272
273			// PENDING_LINE2: look for 0-9, then jump to READING.
274			// Otherwise, pass the keypress through, reset and jump to IDLE.
275			case states.PENDING2:
276				// Look for digit.
277				if ((e.which >= 48 && e.which <= 57)) {
278					state(states.READING);
279
280					$("input").blur();
281
282					processCode(e.which);
283					e.preventDefault();
284					e.stopPropagation();
285					startTimer();
286				}
287				else {
288					clearTimer();
289					scanbuffer = null;
290					state(states.IDLE);
291				}
292				break;
293
294			// READING: Copy characters to buffer until newline, then process the scanned characters
295			case states.READING:
296				processCode(e.which);
297				startTimer();
298				e.preventDefault();
299				e.stopPropagation();
300
301				// Carriage return indicates end of scan
302				if (e.which == 13) {
303					clearTimer();
304					state(states.IDLE);
305					processScan();
306				}
307
308				if (settings.firstLineOnly && e.which == 63) {
309					// End of line 1.  Return early, and eat remaining characters.
310				  state(states.DISCARD);
311				  processScan();
312				}
313				break;
314
315			// DISCARD: Eat up characters until newline, then jump to IDLE
316			case states.DISCARD:
317				e.preventDefault();
318				e.stopPropagation();
319				if (e.which == 13) {
320					clearTimer();
321					state(states.IDLE);
322					return;
323				}
324
325				startTimer();
326				break;
327
328			// PREFIX: Eat up characters until % is seen, then jump to PENDING1
329			case states.PREFIX:
330
331				// If prefix character again, pass it through and return to IDLE state.
332				if (isInPrefixCodes(e.which)) {
333					state(states.IDLE);
334					return;
335				}
336
337				// Eat character.
338				e.preventDefault();
339				e.stopPropagation();
340				// Look for '%'
341				if (e.which == 37) {
342					state(states.PENDING1);
343					scanbuffer = [];
344					processCode(e.which);
345				}
346				// Look for ';'
347				if (e.which == 59) {
348					state(states.PENDING2);
349					scanbuffer = [];
350					processCode(e.which);
351				}
352				startTimer();
353		}
354	};
355
356	// Converts a scancode to a character and appends it to the buffer.
357	var processCode = function (code) {
358		scanbuffer.push(String.fromCharCode(code));
359	};
360
361	var startTimer = function () {
362		clearTimeout(timerHandle);
363		timerHandle = setTimeout(onTimeout, settings.interdigitTimeout);
364	};
365
366	var clearTimer = function () {
367		clearTimeout(timerHandle);
368		timerHandle = 0;
369	};
370
371	// Invoked when the timer lapses.
372	var onTimeout = function () {
373		if (settings.debug) { console.log('Timeout!'); }
374		if (state() == states.READING) {
375			processScan();
376		}
377		scanbuffer = null;
378		state(states.IDLE);
379	};
380
381
382	// Processes the scanned card
383	var processScan = function () {
384
385		if (settings.debug) {
386			console.log(scanbuffer);
387		}
388
389		var rawData = scanbuffer.join('');
390
391		// Invoke rawData callback if defined, a testing hook.
392		if (settings.rawDataCallback) { settings.rawDataCallback.call(this, rawData); }
393
394		var result = parseData(rawData);
395
396		if (result) {
397			// Scan complete. Invoke callback
398			if (settings.success) { settings.success.call(this, result); }
399
400			// Raise success event.
401			$(document).trigger("success.cardswipe", result);
402		}
403		else
404		{
405			// All parsers failed.
406			if (settings.failure) { settings.failure.call(this, rawData); }
407			$(document).trigger("failure.cardswipe");
408		}
409	};
410
411	// Invokes parsers until one succeeds, and returns the parsed result,
412	// or null if none succeed.
413	var parseData = function(rawData) {
414		for (var i = 0; i < settings.parsers.length; i++) {
415		  var ref = settings.parsers[i];
416		  var parser;
417
418		  // ref is a function or the name of a builtin parser
419		  if ($.isFunction(ref)) {
420		    parser = ref;
421		  }
422		  else if (typeof (ref) === "string") {
423		    parser = builtinParsers[ref];
424		  }
425
426		  if (parser != null)
427		  {
428		    var parsedData = parser.call(this, rawData);
429		    if (parsedData == null)
430		      continue;
431
432		    return parsedData;
433		  }
434		}
435
436		// All parsers failed.
437		return null;
438	};
439
440	// Binds the event listener
441	var bindListener = function () {
442		$(document).on("keypress.cardswipe-listener", listener);
443	};
444
445	// Unbinds the event listener
446	var unbindListener = function () {
447		$(document).off(".cardswipe-listener", listener);
448	};
449
450	// Default callback used if no other specified. Works with default parser.
451	var defaultSuccessCallback = function (cardData) {
452		var text = ['Line 1: ', cardData.line1, '\nLine 2: ', cardData.line2, '\nLine 3: ', cardData.line3].join('');
453		alert(text);
454	};
455
456  var isInPrefixCodes = function(arg) {
457    if(!settings.prefixCodes){
458      return false;
459    }
460    return $.inArray(arg,settings.prefixCodes) != -1;
461  };
462
463	// Defaults for settings
464	var defaults = {
465		enabled: true,
466		interdigitTimeout: 250,
467		success: defaultSuccessCallback,
468		failure: null,
469		parsers: [ "generic" ],
470		firstLineOnly: false,
471		prefixCharacter: null,
472		eventSource: document,
473		debug: false
474	};
475
476	// Plugin actual settings
477	var settings;
478
479	// Element on which events are raised.
480	// Normally, this stays set to document, but setting it to another
481	// element makes testing easier.
482	var $eventSource;
483
484	// Apply the Luhn checksum test.  Returns true on a valid account number.
485	// The input is assumed to be a string containing only digits.
486	var luhnChecksum = function (digits) {
487		var map = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9];
488		var sum = 0;
489
490		// Proceed right to left. Even and odd digit positions are handled differently.
491		var n = digits.length;
492		var odd = true;
493		while (n--) {
494			var d = parseInt(digits.charAt(n), 10);
495			if (odd) {
496				// Odd digits used as is
497				sum += d;
498			}
499			else {
500				// Even digits mapped
501				sum += map[d];
502			}
503
504			odd = !odd;
505		}
506
507		return sum % 10 === 0 && sum > 0;
508	};
509
510	// Callable plugin methods
511	var methods = {
512		init: function (options) {
513			settings = $.extend({}, defaults, options);
514
515			// Is a prefix character defined?
516			if (settings.prefixCharacter) {
517
518        // Check if prefix character is an array, if its not, convert
519        var isPrefixCharacterArray = Object.prototype.toString.call(settings.prefixCharacter) === '[object Array]';
520        if(!isPrefixCharacterArray){
521          settings.prefixCharacter = [settings.prefixCharacter];
522        }
523
524        settings.prefixCodes = [];
525        $(settings.prefixCharacter).each(function(){
526          if (this.length != 1){
527  					throw 'prefixCharacter must be a single character';
528          }
529
530          // convert to character code
531          settings.prefixCodes.push(this.charCodeAt(0));
532        });
533			}
534
535			$eventSource = $(settings.eventSource);
536
537			// Reset state
538			clearTimer();
539			state(states.IDLE);
540			scanbuffer = null;
541			unbindListener();
542
543			if (settings.enabled)
544				methods.enable();
545		},
546
547		disable: function () {
548			unbindListener();
549		},
550
551		enable: function () {
552			bindListener();
553		}
554	};
555
556	// The Luhn checksum function is available to the client if needed.
557	plugin.luhnChecksum = luhnChecksum;
558
559	// Attach internal functions to plugin for easier testing. These aren't intended for
560	// use in production, hence the underscore security.
561
562	// Get all states
563	plugin._states = function() {
564		return states;
565	};
566
567	// Get names of states
568	plugin._stateNames = function() {
569		return stateNames;
570	};
571
572	// Read-only access to the current state
573	plugin._state = function() {
574		return state();
575	};
576
577	// Read-only access to the settings supplied to init method
578	plugin._settings = function() {
579		return settings;
580	};
581
582	// Invoke parsers on supplied data
583	plugin._parseData = function(data) {
584		return parseData(data);
585	};
586
587	plugin._builtinParsers = function() {
588		return builtinParsers;
589	};
590
591	// Attach plugin to jQuery object.
592	$.cardswipe = plugin;
593
594}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.