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.