1(function (FP) { 2 3 'use strict'; 4 5 var d = document, 6 w = window; 7 8 // We set some basic vars 9 FP.v = {}; 10 fp.track_queue = []; 11 fpdata.activity = { total : 0, last : 0, current : 0 }; 12 fpdata.refreshed = false; 13 14 if ( document.referrer == document.location.origin + document.location.pathname ) { 15 fpdata.refreshed = true; 16 } else { 17 if ( performance.navigation ? performance.navigation.type === performance.navigation.TYPE_RELOAD : performance.getEntriesByType('navigation')[0].type === 'reload' ) fpdata.refreshed = true; // deprecated method first, the new one later 18 } 19 20 // fires function every X ms with an option to fire them ADDITIONALLY on start ("leading") and after events ("trailing") 21 FP.throttle = function (func, wait, options) { 22 var context, args, result; 23 var timeout = null; 24 var previous = 0; 25 if (!options) options = {}; 26 27 var later = function later() { 28 previous = options.leading === false ? 0 : Date.now(); 29 timeout = null; 30 result = func.apply(context, args); 31 if (!timeout) context = args = null; 32 }; 33 34 return function () { 35 var now = Date.now(); 36 if (!previous && options.leading === false) previous = now; 37 var remaining = wait - (now - previous); 38 context = this; 39 args = arguments; 40 41 if (remaining <= 0 || remaining > wait) { 42 if (timeout) { 43 clearTimeout(timeout); 44 timeout = null; 45 } 46 47 previous = now; 48 result = func.apply(context, args); 49 if (!timeout) context = args = null; 50 } else if (!timeout && options.trailing !== false) { 51 timeout = setTimeout(later, remaining); 52 } 53 54 return result; 55 }; 56 }; 57 58 FP.isObjectEmpty = function (obj) { 59 return obj.constructor === Object && Object.keys(obj).length === 0; 60 }; 61 62 FP.isNumeric = function (n) { 63 return !isNaN(parseFloat(n)) && isFinite(n); 64 }; 65 66 FP.reduce = function( arr ){ 67 return arr.reduce( function(accumulator, val){ return val ? accumulator + val : accumulator } ); 68 }; 69 70 FP.remove = el => { if (el) el.parentNode.removeChild(el); }; 71 72 FP.findFirst = (e, c) => { 73 if ( c === null ) return null; 74 if ( !e ) return false; 75 c = c || document; 76 return c.querySelector(e); 77 }; 78 79 FP.addAction = function( action_name, fn ){ 80 81 let arr = Array.isArray(action_name) ? action_name : [action_name]; // make sure that trigger is in an array 82 83 arr.forEach(function ( name , i) { 84 if ( ! fp.actions[name] ) fp.actions[name] = []; 85 fp.actions[name].push( fn ); 86 }); 87 } 88 89 FP.doActions = ( action_name, args_o = null, cb ) => { 90 if ( fp.actions[action_name] ) fp.actions[action_name].forEach( fn => { 91 let return_val = fn( args_o ); 92 if ( typeof return_val !== 'undefined' ) args_o = return_val; 93 } ); 94 if ( cb ) cb( args_o ); 95 // TODO: add in this place a function that sends the collected payload to server 96 } 97 98 FP.hasActions = action_name => { 99 return fp.actions[action_name] && fp.actions[action_name].length > 0; 100 }; 101 102 function shouldCustomScriptLoadHere( geo ){ 103 104 let geo_method = geo[0], 105 geo_countries = geo[1]; 106 107 if ( fp.geo && geo_method && geo_countries ) { // we check fp.geo to prevent situations when user disabled geo but didn't save new tools settings 108 109 if ( fp.ready ) { 110 return geo_method == 'incl' ? geo_countries.includes( fpdata.country ) : ! geo_countries.includes( fpdata.country ); 111 } 112 return false; 113 } 114 115 return true; 116 } 117 118 function shouldScriptLoadHere( id ){ 119 120 if ( fp.geo && fp[id].limit_country ) { // we check fp.geo to prevent situations when user disabled geo but didn't save new tools settings 121 122 if ( fp.ready ) { 123 124 if ( fp[id].limit_country.method == 'excl' ) { 125 return ! fp[id].limit_country.countries.includes( fpdata.country ); 126 } else { 127 return fp[id].limit_country.countries.includes( fpdata.country ); 128 } 129 } 130 131 return false; 132 } 133 134 return true; 135 }; 136 137 FP.isAllowedToLoad_basic = function( id, force_load, permissions_a, geo_a ){ 138 139 // STOP if we don't have all the basic data yet 140 if ( ! fp.ready ) return false; 141 142 // STOP if the script has already loaded 143 if ( fp.loaded.includes(id) ) return false; 144 145 // START if the load is forced 146 if ( force_load ) return true; 147 148 // STOP if the tab is not in focus AND we are NOT on WooCommerce checkout page 149 if ( fpdata.page_type !== 'Woo Order Received' ) { 150 if ( document.hidden ) return false; 151 } 152 153 // STOP tracking an excluded visitor (MUST be after checking FORCE LOAD) 154 if ( ! fp.vars.track_current_user ) return false; 155 156 // STOP if the script is set NOT to load in the current location 157 if ( geo_a && ! shouldCustomScriptLoadHere( geo_a ) ) return false; 158 159 // STOP if any permissions need to be given in this location...
160 if ( fp.notice.enabled && fp.notice.mode == 'optin' && ! fpdata.cookies && permissions_a.length > 0 ) return false; 161 162 // STOP if required permissions are not given 163 if ( fpdata.cookies && permissions_a.length > 0 ) { 164 if ( ! permissions_a.every( permission => fpdata.cookies[permission] ) ) return false; 165 } 166 167 return true; 168 }; 169 170 FP.isAllowedToLoad = function( module_id, permissions_a, required_a, nr, tool_does_not_use_cookies = false ) { 171 172 var id = nr ? module_id + nr : module_id; 173 174 // STOP if we don't have all the basic data yet 175 if ( ! fp.ready ) return false; 176 177 // STOP if we have no settings for this tool 178 if ( ! fp[id] ) return false; 179 180 // STOP if the script has already loaded 181 if ( fp.loaded.includes(id) ) return false; 182 183 // STOP if the required data is missing 184 if ( required_a.some( req => ! fp[id][req] ) ) return false; 185 186 // START if the load is forced 187 if ( fp[id].force_load ) return true; 188 189 // STOP if the tab is not in focus AND we are NOT on WooCommerce checkout page 190 if ( fpdata.page_type !== 'Woo Order Received' ) { 191 if ( document.hidden ) return false; 192 } 193 194 // STOP tracking an excluded visitor (MUST be after checking FORCE LOAD) 195 if ( ! fp.vars.track_current_user ) return false; 196 197 // STOP if the script is set NOT to load in the current location 198 if ( ! shouldScriptLoadHere( id ) ) return false; 199 200 // START 201 // if the tool doesn't use cookies 202 // or if it is set to disregard cookie choices 203 if ( tool_does_not_use_cookies || fp[id].disreg_cookies ) return true; 204 205 // STOP if we wait for consent but some permissions need to be given 206 if ( fp.notice.enabled && fp.notice.mode == 'optin' && ! fpdata.cookies && permissions_a.length > 0) return false; 207 208 // STOP if user chose their cookie preferences but some required permissions are missing 209 if ( fp.notice.enabled && fpdata.cookies && permissions_a.length > 0 ) { 210 if ( ! permissions_a.every( permission => fpdata.cookies[permission] ) ) return false; 211 } 212 213 return true; 214 }; 215 216 FP.updateScrollValues = function(){ 217 218 fpdata.scrolled = fpdata.scrolled || {}; 219 220 fpdata.scrolled.current = Math.round( d.documentElement.scrollTop / ( d.documentElement.scrollHeight - d.documentElement.clientHeight ) * 100 ) || 0; 221 fpdata.scrolled.current_px = d.documentElement.scrollTop || 0; 222 fpdata.scrolled.max = fpdata.scrolled.max || fpdata.scrolled.current; 223 fpdata.scrolled.max_px = fpdata.scrolled.max_px || fpdata.scrolled.current_px; 224 225 if ( fpdata.scrolled.current > fpdata.scrolled.max ) { 226 fpdata.scrolled.max = fpdata.scrolled.current; 227 fpdata.scrolled.max_px = fpdata.scrolled.current_px; 228 } 229 } 230 231 FP.startActivityTimer = function () { 232 233 fpdata.activity.last = 0; 234 235 if (!FP.v.activityTimerRunning) { 236 FP.v.activityTimerRunning = true; 237 FP.v.activityTimer = setInterval(function () { 238 239 fpdata.activity.total++; 240 fpdata.activity.current++; 241 fpdata.activity.last++; 242 243 FP.doActions( 'active_time_tick' ); 244 245 if ( fpdata.activity.last >= 15 ) FP.stopActivityTimer(); // stop timer after 15 secs from last activity 246 }, 1000); 247 } 248 }; 249 250 FP.stopActivityTimer = function () { 251 if (FP.v.activityTimerRunning) { 252 FP.v.activityTimerRunning = false; 253 clearInterval(FP.v.activityTimer); 254 } 255 }; 256 257 FP.encode = function (string) { 258 return window.btoa(string).split('=').join('') 259 } 260 261 FP.detectAddedElement = callback => { 262 263 const observer = new MutationObserver( mutations => { 264 mutations.forEach( mutation => { 265 if ( mutation.type === 'childList' ) { 266 callback( mutation.addedNodes ); 267 } 268 } ); 269 } ); 270 271 observer.observe( document.body, { 272 childList: true, 273 subtree: true 274 } ); 275 } 276 277 FP.initCondTracking = ( events_settings, cb ) => { 278 279 if ( ! ( events_settings && fp.atrig ) ) return; 280 if ( ! fp.atrig.actions ) fp.atrig.actions = {}; 281
282 events_settings.forEach( e => { 283 284 if ( ! fp.atrig.actions[e.atrig_id] ) fp.atrig.actions[e.atrig_id] = []; 285 286 fp.atrig.actions[e.atrig_id].push( { 287 'func' : cb.bind( this, e ), 288 'repeat' : e.repeat 289 } ); 290 }); 291 } 292 293 /* V8 mods - START */ 294 295 // Check if WPFP can use cookies 296 297 function check_if_can_save_wpfp_cookies( first_check = false ) { 298 299 // save previous state 300 let prev_check = !! fp.vars.can_save_wpfp_cookies; // "!!" changes undefined into false 301 302 // get current state 303 fp.vars.can_save_wpfp_cookies = ! fp.notice.enabled || fp.notice.mode == 'notify' || fp.notice.mode == 'hide' || ( fp.notice.mode == 'optout' && ! fpdata.cookies ) || ( fp.notice.mode == 'optin' && fpdata.cookies && fpdata.cookies.stats ); 304 305 // check if the state has changed 306 fp.vars.consents_changed = first_check ? false : prev_check != fp.vars.can_save_wpfp_cookies; 307 } 308 309 check_if_can_save_wpfp_cookies( true ); 310 311 // Get session data from cookies 312 313 function get_session_data_from_cookies(){ 314 315 let session_cookie = FP.readCookie('fp_current_session'); 316 if ( session_cookie ) fpdata.current_session = JSON.parse(session_cookie); 317 318 319 } 320 321 get_session_data_from_cookies(); 322 323 324 325 function create_new_single_session_data(){ 326 327 fpdata.current_session = {}; 328 329 330 331 FP.setCookie( 'fp_current_session', JSON.stringify(fpdata.current_session), 0, 30 ); // 30 mins 332 } 333 334 function update_single_session_data(){ 335 if ( ! fpdata.current_session ) return; 336 337 FP.setCookie( 'fp_current_session', JSON.stringify(fpdata.current_session), 0, 30 ); // 30 mins 338 } 339 340 341 342 // Set or update session data when the page loads 343 function set_session_data(){ 344 345 if ( fp.vars.can_save_wpfp_cookies ) { 346 347 fpdata.new_session = ! fpdata.current_session; 348 349 ! fpdata.current_session ? create_new_single_session_data() : update_single_session_data(); 350 351 352 } else { 353 fpdata.new_session = ! fpdata.refreshed && ! window.sessionStorage['fp_old_session'] && ( window.history.length == 1 || ( window.history.length == 2 && ! document.referrer ) ); 354 } 355 356 fpdata.new_tab = ! fpdata.new_session && ! window.sessionStorage['fp_old_session']; 357 window.sessionStorage['fp_old_session'] = true; // removed on a new tab 358 }; 359 360 set_session_data(); 361 362 // CREATE, UPDATE OR REMOVE SESSION DATA 363 // triggered after users change cookie preferences or when a page gains focus 364 365 FP.updateSessionData = () => { 366 367 check_if_can_save_wpfp_cookies(); 368 369 if ( fp.vars.consents_changed ) { 370 371 // if we can no longer use session cookies 372 if ( ! fp.vars.can_save_wpfp_cookies ) { 373 374 // delete everything 375 FP.deleteCookie( 'fp_current_session' ); 376 delete fpdata.current_session; 377 378 379 // if we can now use session cookies 380 } else { 381 382 // get latest sesion data (in case user agreed to cookies on a different tab) 383 get_session_data_from_cookies(); 384 385 // set new data if it wasn\'t set before (when a user has just chosen sth in the banner) 386 ! fpdata.current_session ? create_new_single_session_data() : update_single_session_data(); 387 388 }; 389 390 } else { 391 // get latest sesion data (in case user visited a different tab before opening this one) 392 get_session_data_from_cookies(); 393 } 394 } 395 396 /* V8 mods - END */ 397 398 FP.isClickTarget = function( selector ){ 399 return ( ! fpdata.clicked.link && ! fpdata.clicked.middle && fpdata.clicked.element.matches( selector ) ) || ( fpdata.clicked.link && fpdata.clicked.link.element.matches( selector ) ); 400 } 401 402 // the difference between this Fn and isAnchorLinkToCurrentPage is that this one doesn't check if the link actually works 403 function isLinkToCurrentPage (link) { 404 let target_url_obj = new URL( link.href ); 405 return target_url_obj.origin + target_url_obj.pathname == location.origin + location.pathname; 406 } 407 408 // checks if the link is an anchor to an EXISTING element on the same page 409 function isAnchorLinkToCurrentPage (link){ 410 411 let url = new URL( link.href ), 412 el = false; 413 414 try { 415 el = FP.findFirst( url.hash ); 416 } catch (e) { 417 el = false; 418 } 419 420 return !! el; 421 }; 422 423 function isOutboundLink (el) { 424 return el.href.indexOf(location.host) == -1 && el.href.indexOf('mailto:') != 0 && el.href.indexOf('tel:') != 0 && el.href.indexOf('http') == 0; 425 }; 426 427 function trackClicks(e, is_middle_click) { 428 429 let redirect_stopped = false; 430 431 if ( e.target.classList.contains('fupi_click_stopped') ) return; 432 433 // PREVENT MULTI CLICKS
434 // Do not run this function if a visitor clicks the element multiple times in a short time-span 435 var now = Date.now(); 436 if ( ! ( ! FP.v.lastClick || now - FP.v.lastClick > fp.vars.dblclck_time ) ) return; 437 FP.v.lastClick = now; 438 439 // PREVENT REDIRECTS 440 441 // prevent default if a visitor clicked a link that is to be opened on the current tab 442 var link_el = e.target.closest('a[href]'); 443 444 if ( link_el && ! is_middle_click && link_el.target != '_blank' && fp.vars.link_click_delay ){ 445 e.preventDefault(); // prevents redirects 446 e.stopPropagation(); // prevents other event listeners from firing 447 e.target.classList.add('fupi_click_stopped'); 448 e.target.style.pointerEvents = 'none'; 449 redirect_stopped = true; 450 if ( fp.vars.debug ) console.log('[FP] link-click stopped'); 451 }; 452 453 // GET CLICK DATA 454 455 fpdata.clicked = { 456 'element' : e.target, 457 'middle' : is_middle_click, 458 }; 459 460 if ( link_el && link_el.href.length > 1 ) { 461 462 fpdata.clicked.link = {}; 463 fpdata.clicked.link.element = link_el; 464 fpdata.clicked.link.href = link_el.href; 465 fpdata.clicked.link.is_to_current_page = isLinkToCurrentPage(link_el); 466 fpdata.clicked.link.target_blank = link_el.target == '_blank'; 467 468 fpdata.clicked.link.is_outbound = isOutboundLink(link_el); 469 fpdata.clicked.link.is_anchor = isAnchorLinkToCurrentPage(link_el); 470 471 if ( link_el.href.includes('mailto:') ){ 472 fpdata.clicked.link.is_email = true; 473 fpdata.clicked.link.safe_email = link_el.href.replace('mailto:', '').split('@')[0]; 474 } 475 476 if ( link_el.href.includes('tel:') ){
477 fpdata.clicked.link.is_tel = true; 478 fpdata.clicked.link.safe_tel = link_el.href.replace('tel:','').slice(-5); 479 } 480 } 481 482 // RUN QUEUED FNs 483 484 FP.doActions( 485 'click', 486 false, // no slug 487 () => { // cb after all functions are run 488 if ( fpdata.clicked.element.classList.contains('fupi_click_stopped') && typeof fpdata.clicked.element.click == 'function' ) { 489 fpdata.clicked.element.click(); 490 fpdata.clicked.element.classList.remove('fupi_click_stopped'); 491 e.target.style.pointerEvents = ''; 492 if ( fp.vars.debug ) console.log('[FP] Initiated click event after all FP functions'); 493 } 494 } 495 ); 496 }; 497 498 function getAttributesFromStr(str){ 499 500 let clean_str = str.replaceAll( "\'", "'").replaceAll('\"', '"'), 501 parts = clean_str.trim().split(' '), // spaces occur in unexpected places! 502 ok_parts = parts.filter( v => v ), // remove empty values 503 ret_val = {}; 504 505 for ( var i = 0; i < ok_parts.length; i++ ) { 506 507 let part = ok_parts[i], 508 eq_index = part.indexOf('='), 509 last_char = part[part.length-1]; 510 511 // if we have an equal sign 512 // (this means that the value before the equal sign is the key and the one after is the value) 513 if ( eq_index != -1 ){ 514 515 // we check if the last element of the current part is an apostrophe. If it isn\'t then it means we split the value in two or more pieces and need to join it with the next string parts 516 while ( ! ( last_char == '"' || last_char == "'" ) && i+1 <= ok_parts.length ) { 517 part += ' ' + ok_parts[i+1]; 518 last_char = part[part.length-1]; 519 i++; 520 } 521 522 // if we have the key and the whole value 523 if ( last_char == '"' || last_char == "'" ) { 524 525 // split on the first occurance of "=" sign 526 let key_val = part.split(/=(.*)/s); 527 528 // remove empties 529 key_val = key_val.filter( v => v ); 530 531 // remove apostrophes 532 if ( key_val[1].includes('"') ){ 533 key_val[1] = key_val[1].replaceAll('"', ''); 534 }; 535 536 if ( key_val[1].includes("'") ){ 537 key_val[1] = key_val[1].replaceAll("'", ''); 538 } ; 539 540 // save in an array 541 ret_val[key_val[0]] = key_val[1]; 542 } 543 544 // if we don't have an equal sign this is a single value 545 } else { 546 ret_val[part] = true; 547 } 548 }; 549 550 return ret_val; 551 }; 552 553 // Add blocked scripts to HTML 554 function addScriptsToDOM() { 555 556 fp.blocked_scripts.forEach( scr_data => { 557 558 let url = scr_data[0], 559 attr_s = scr_data[1], 560 scr_id = scr_data[2], 561 force_load = scr_data[3], 562 permissions_a = scr_data[4], 563 geo_a = scr_data[5], 564 type = scr_data[6] || false; 565 566 if ( FP.isAllowedToLoad_basic( scr_id, force_load, permissions_a, geo_a ) ) { 567 if ( ! url && attr_s == 'empty' ){ 568 FP.loadScript( scr_id ); 569 } else { 570 let attr_o = getAttributesFromStr(attr_s); 571 FP.getScript( url, ()=>{fp.loaded.push( scr_id ) }, attr_o, type ); 572 } 573 } 574 } ) 575 } 576 577 // FIRE TAGS WHEN THE DOCUMENT GETS AND LOSES FOCUS 578 579 function doPageShowActions(){ 580 581 if ( ! fp.vars.page_show_tracked ){ 582 583 fp.vars.page_show_tracked = true; 584 fp.vars.page_hide_tracked = false; 585 586 fpdata.doc_in_focus = true; 587 588 FP.startActivityTimer(); 589 590 var cookies = FP.readCookie('fp_cookie'); 591 fpdata.cookies = cookies ? JSON.parse(cookies) : false; 592 593 FP.updateConsents(); 594 595 FP.updateSessionData(); 596 597 FP.sendEvt( 'fp_load_scripts' ); 598 599 if ( fp.actions.page_in_focus ) FP.doActions('page_in_focus'); 600 } 601 } 602 603 function doPageHideActions(){ 604 if ( ! fp.vars.page_hide_tracked ) { 605 fp.vars.page_hide_tracked = true; 606 fp.vars.page_show_tracked = false; 607 608 fpdata.doc_in_focus = false; 609 FP.stopActivityTimer(); 610 if ( fp.actions.close ) FP.doActions('page_in_blur'); 611 fpdata.activity.current = 0; 612 } 613 } 614 615 // ON DYNAMIC URL CHANGES 616 // URL is set every second with setInterval() and not "active_time_tick", to make the check even when the page is not in focus 617 618 let currentUrl = location.href; 619 620 setInterval( () => { 621 if ( location.href !== currentUrl ) { 622 currentUrl = location.href; 623 FP.doActions( 'url_change' ); 624 } 625 }, 1000); 626 627 // pagehide/show fires on some events but is cross-browser 628 // !! it fires BEFORE visibilitychanges 629 // !! when the pageshow event fires the document obj doesn't have updated cookies or localStarage yet, thus we don't use it for "show" 630 window.addEventListener( "pagehide", e => { doPageHideActions() }, false ); 631 // window.addEventListener( "pageshow", e => { doPageShowActions('pageshow') }, false ); visibilitychange is universal 632 633 // beforeunload doesn't work with prev/next page buttons and has problems on mobiles 634 window.addEventListener( "beforeunload", e => { doPageHideActions() }, false ); 635 636 // visibilitychanges fires on all events but has problems on mobiles and in safari 637 // !! fires AFTER pagehide/show 638 // !! when the visibilitychange event fires for "show" the document obj already has updated cookies and localStarage 639 document.addEventListener( 'visibilitychange', function () { 640 document.hidden ? doPageHideActions() : doPageShowActions('visibilitychange'); 641 //if ( fp.page_visibility ) FP.doActions( 'page_visibility' ); 642 }, false); 643 644 // load custom/extra scripts to DOM 645 document.addEventListener( 'fp_load_scripts', addScriptsToDOM ); 646 647 function formSubmitEvents( {target} ){
648 fpdata.submitted_form = { 'element' : target }; 649 FP.doActions( 'form_submit' ); 650 } 651 652 // ON DOM LOADED 653 document.addEventListener( 'DOMContentLoaded', ()=>{ 654 655 // fp.vars.dom_loaded_time = Date.now(); 656 addScriptsToDOM(); 657 658 // init actions waiting for DOM loaded 659 setTimeout ( ()=>{ FP.doActions( ['dom_loaded'] ) }, 250 ); // we must add a small delay to make sure that all triggers are hooked 660 661 // start listening to form submits 662 setTimeout( ()=>{ document.addEventListener('submit', formSubmitEvents ) }, fp.vars.formsubm_trackdelay ? fp.vars.formsubm_trackdelay * 1000 : 1000 ); 663 664 // start listening to DOM modifications 665 if ( fp.vars.use_mutation_observer ) FP.detectAddedElement( addedNodes => { 666 let els = FP.nl2Arr( addedNodes ); 667 if ( els.length > 0 ) FP.doActions( ['dom_modified'], els ); 668 } ); 669 }); 670 671 672 // ON MOUSE MOVE FOR USER ACTVITY TRACKER 673 document.addEventListener( 'mousemove', FP.throttle( FP.startActivityTimer, 500 ) ); // This starts the user's activity timer 674 675 if ( ! document.hidden) { 676 FP.startActivityTimer(); 677 fpdata.doc_in_focus = true; 678 fp.vars.page_show_tracked = true; 679 fp.vars.page_hide_tracked = false; 680 }; 681 682 // ON LEFT CLICK 683 window.addEventListener( 'click', function (e) { 684 if ( fp.actions.click ) trackClicks(e, false); 685 }, true); 686 687 // ON MIDDLE CLICK 688 window.addEventListener( 'mouseup', function(e) { 689 if ( fp.actions.click && e.button == 1) trackClicks(e, true); 690 }); 691 692})(FP);
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.