freecog.net

lplfixup.user.js

Also see Install.

   1 // ==UserScript==
   2 // @name           LPLFixup
   3 // @version        12
   4 // @namespace      http://freecog.net/2006/
   5 // @description    Fixes a variety of annoyances at the La Crosse Public Library web site.
   6 // @include        http://lacrosselibrary.org/*
   7 // @include        http://www.lacrosselibrary.org/*
   8 // @include        http://lacrosse.lib.wi.us/*
   9 // @include        http://www.lacrosse.lib.wi.us/*
  10 // @include        http://lpl.lacrosse.lib.wi.us/*
  11 // @include        http://lpl.lacrosse.lib.wi.us:81/*
  12 // @include        http://lplcat.lacrosse.lib.wi.us/rpa/webauth.exe*
  13 // @include        http://lplcat.lacrosse.lib.wi.us:8080/rpa/webauth.exe*
  14 // ==/UserScript==
  15 
  16 //GM_log("LPLFixup running.");
  17 
  18 // Changelog
  19 /*
  20 
  21 Version 12, 10 October 2007:
  22     Fixed a bug that would cause the script to break if you had no saved information.
  23 
  24 Version 11, 18 February 2007:
  25     Added support for filling of the "Remote Patron Authentication" form.
  26     Minor aesthetic improvements.
  27     Added fixing of the "Reader's Guide" link in the Information Toolbox tab so that
  28         the user is not prompted to select a library (New pref: boolean 
  29         "fix_readers_guide_link")
  30     Fixed a bug in the rewriting of javascript: URLs.
  31 
  32 Version 10, 7 February 2007:
  33     New options dialog; many more exposed options with extended explanations.
  34     Renamed pref "fix_click_for_more_search_options_text" to "fix_more_search_options_link".
  35 
  36 Version 9, 4 February 2007:
  37     Bugfix: "www" subdomain versions of URLs for which this script is included added.
  38 
  39 Version 8, 20 Jan 2007:
  40     User preferences (e.g., the checkboxes at the bottom of the page) are now automatically
  41         propagated to pages in other windows or tabs.
  42 
  43 Version 7, 5 Jan 2007:
  44     Added better support for "My List" page titles.
  45     Finally got the session timeout prevention mechanism working.  (New pref: boolean 
  46         "maintain_session"; New pref: int "maintain_session_interval"; New pref:  int
  47         "session_timeout_interval")
  48 
  49 Version 6, 1 Jan 2007:
  50     Added JavaScript link rewriting (New pref: boolean "fix_js_links", disabled 
  51          by default)  This is disabled because it may cause strange behavior--
  52          more testing is necessary.
  53     Added fixing of the "More Search Options" link, which is by default called
  54          "Click For More Search Options".  (New pref: boolean 
  55          "fix_click_for_more_search_options_text")
  56     Added fixing of how the search history page displays "More Search Options" 
  57          searches as "Click For;More Search Options"  (New pref: boolean 
  58          "fix_more_search_options_history")
  59 
  60 Version 5, 1 Jan 2007:
  61     Fixed bug that attempted to parse the URL on a logout page.
  62     Added support for infering the title on search form pages.
  63     Added some comments.
  64     Restructured the title code.
  65     Added fixing of the "Reader's Guide" link under "Information Toolbox" so that it looks
  66          like a normal link, instead of a visited one.  (New pref: boolean 
  67          "fix_readers_guide_link_style")
  68     New pref: boolean "fix_title" -- globabl flag for fixing of page titles
  69     Added fixing of the "Local news index&Fast Facts" tab text.
  70     Added the Community Resources site to the include list: http://lpl.lacrosse.lib.wi.us:81/*
  71     Added support for titles for Community Resources record pages. 
  72     Happy new year.
  73 
  74 Version 4, 31 Dec 2006:
  75     Added fixing of the "More Search Options" page title, which is "Click Here For ;More
  76          Search Options" by default.  (New pref: boolean "fix_more_search_options_title")
  77     Fixed typo: 'Accound Overview' -> 'Account Overview'
  78     Minor styling enhancements.
  79     Reused UI text moved to `ui_strings` dictionary.
  80     Added fixing of the background image on pages with the flowery red image (which tiles
  81          on wide monitors).
  82     Removed use of UTF-8 Unicode literals in favor of escape sequences (due to a stupid 
  83          Greasemonkey bug).
  84     Fixed a bug that caused POST pages to have incorrect titles missing the last two letters.
  85 
  86 Version 3, 17 Dec 2006:
  87     Added document title inference.  (New pref: boolean "end_titles_with_lpl".)
  88     Added null removal. (New pref: boolean "hide_null".)
  89     Added non-link style fixer (New pref: boolean "fix_nonlink_styles".)
  90 
  91 Version 2, 17 Dec 2006: Initial public release
  92 
  93 version 1: Initial version
  94 
  95 */
  96 
  97 
  98 // Unicode symbol quick reference
  99 /*
 100 Hyphen: \u2010
 101 EN-Dash: \u2013
 102 EM-Dash: \u2014
 103 Left single quotation mark: \u2018
 104 Right single quotation mark: \u2019
 105 Left double quotation mark: \u201c
 106 Right double quotation mark: \u201d
 107 Interrobang: \u203d                   // Just 'cause I can.
 108 */
 109 
 110 
 111 const DEBUG = GM_getValue("debug", false);
 112 // Don't access the Firebug console through the unsafeWindow object?
 113 const SAFE_DEBUG = GM_getValue("safe_debug", false);
 114 
 115 function log() {
 116     if (DEBUG) {
 117         if (!SAFE_DEBUG && unsafeWindow.console) {
 118             // It's nice to bypass Greasemonkey's logging function because
 119             // that function prepends a long namespace to the logged value,
 120             // wasting screen space.
 121             unsafeWindow.console.log.apply(unsafeWindow.console, arguments);
 122         } else {
 123             GM_log.apply(null, arguments);
 124         }
 125     }
 126 };
 127 
 128 
 129 // Oh, how I long,
 130 //    for MochiKit dot DOM.
 131 function El(name /* children... */) {
 132     var a, el = document.createElement(name.match(/^[^.#$\s]+/)[0]);
 133     for (var i = 1; i < arguments.length; i++) {
 134         a = arguments[i];
 135         if (a.__dom__) a = a.__dom__(el);
 136         else if (a.dom) a = a.dom(el);
 137         switch (typeof a) {
 138             case 'string':
 139             case 'number':
 140                 a = document.createTextNode(a);
 141             default:
 142                 if (a) el.appendChild(a);
 143                 break;
 144         }
 145     }
 146     var id = name.match(/#([^\.#\s]+)/);
 147     if (id) el.id = id[1];
 148     var classes = name.match(/\.[^.#$\s]+/g);
 149     if (classes)
 150         el.className = classes.map(function(c) {
 151             return c.slice(1); }).join(' ');
 152     var style = name.match(/\$(.+)$/);
 153     if (style) el.setAttribute('style', style[1]);
 154     return el;
 155 };
 156 
 157 
 158 
 159 
 160 function Prefs(options) {   
 161     var self = this;
 162     // Table mapping pref names to arrays of listener functions 
 163     var listeners = {};
 164     // Stores the last known value of each pref that is listened
 165     // for so that changes can be detected.
 166     var cache = {};
 167     // An array of the names that are stored in the cache.
 168     var cached_prefs = [];
 169     
 170     function check_for_update(name) {
 171         // List of names of prefs to check if they have changed.
 172         var names_to_check = (name) ? [name] : cached_prefs;
 173         for (var i = 0; i < names_to_check.length; i++) {
 174             name = names_to_check[i];
 175             var old_value = cache[name];
 176             var new_value = self[name];
 177             if (old_value !== new_value) { // There has been a change
 178                 log("Change detected for pref " + name);
 179                 log("  old value: %o", old_value);
 180                 log("  new value: %o", new_value);
 181                 var funcs = listeners[name];
 182                 if (funcs) {
 183                     for (var j = 0; j < funcs.length; j++) {
 184                         funcs[j](new_value, old_value, name);
 185                     }
 186                 }
 187                 cache[name] = new_value;
 188             }
 189         }
 190     }
 191     
 192     if (options.live_update) {
 193         window.setInterval(function() {
 194             check_for_update();
 195         }, options.live_update_interval || 200);
 196     }
 197     
 198     this.create = function(name, default_) {
 199         if ("create|make_checkbox|listen".indexOf(name)>-1)
 200             throw "The name " + name + " is reserved.";
 201         
 202         // `is_number` is used to enable a hack that stores
 203         // numbers as strings, allowing the storage of any
 204         // JavaScript number, not just ints.
 205         var is_number = (typeof(default_) == 'number');
 206         
 207         self.__defineGetter__(name, function() {
 208             var val = GM_getValue(name, default_);
 209             if (is_number) val = parseFloat(val, 10);
 210             return val;
 211         });
 212         
 213         self.__defineSetter__(name, function(value) {
 214             log("Pref " + name + " set");
 215             cache[name] = self[name];
 216             var set_value = (is_number) ? '' + value : value;
 217             GM_setValue(name, set_value);
 218             if (name in listeners) {
 219                 check_for_update(name);
 220             }
 221             return value;
 222         });
 223         
 224         log("Pref created: " + name);
 225     };
 226     
 227     // Set a function to be called whenever the pref is
 228     // changed, with its value, old value, and name.
 229     this.listen = function(name, func) {
 230         if (!(name in listeners)) { // Hasn't been listened to before.
 231             listeners[name] = [];
 232             // Initialize the cache so changes can be detected.
 233             cache[name] = self[name];
 234             cached_prefs.push(name);
 235         }
 236         listeners[name].push(func);
 237         log("Listener added to " + name);
 238     };
 239     
 240     // Make a checkbox that is linked to the pref `name`.
 241     // Boolean prefs only, please.
 242     this.make_checkbox = function(name) {
 243         var cb = document.createElement('input');
 244         cb.setAttribute('type', 'checkbox');
 245         cb.checked = self[name];
 246         cb.addEventListener('DOMActivate', function() {
 247             self[name] = this.checked;
 248         }, false);
 249         // Register
 250         self.listen(name, function(value, old_value, name) {
 251             cb.checked = value;
 252         });
 253         return cb;
 254     };
 255     
 256     // Link an <input> or <textarea> to a pref.
 257     // Textual prefs only.
 258     this.link_text_field = function(name, input, overwrite/*=false*/) {
 259         function update() { self[name] = input.value; }
 260         input.addEventListener("change", update, false);
 261         input.addEventListener("keypress", update, false);
 262         input.addEventListener("DOMFocusOut", update, false);
 263         input.addEventListener("DOMActivate", update, false);
 264         document.addEventListener("unload", update, false);
 265         
 266         self.listen(name, function(value, old_value, name) {
 267             input.value = value;
 268         });
 269         
 270         if (overwrite) { // Overwrite
 271             input.value = self[name];
 272         } else {
 273             self[name] = input.value;
 274         }
 275     };
 276 }
 277 
 278 
 279 function now() {
 280     return (new Date()).getTime();
 281 }
 282 
 283 
 284 
 285 
 286 
 287 
 288 
 289 
 290 
 291 // Is this page part of the abominable Horizon iPac system?
 292 var is_ipac_page = document.location.href.match(/lpl.lacrosse.lib.wi.us\//i);
 293 
 294 function is_login_page() {
 295     return is_ipac_page && document.forms[0] && document.forms[0].name == 'security';
 296 }
 297 
 298 function is_remote_auth_page() {
 299     return document.location.href.match(/http:\/\/lplcat\.lacrosse\.lib\.wi\.us\/rpa\/webauth\.exe/i);
 300 }
 301 
 302 
 303 // Detect a failed login attempt.
 304 function login_failed() {
 305     return document.body.textContent.indexOf("Login failed.  Please check your barcode and pin and try again.") > -1;
 306 };
 307 
 308 (function main(){
 309 
 310 var prefs = new Prefs({live_update: true});
 311 prefs.create("change_fields", true);
 312 prefs.create("remember_info", false);
 313 prefs.create("disable_timer", true);
 314 prefs.create("auto_login", false);
 315 prefs.create("barcode", '');
 316 prefs.create("pin", '');
 317 prefs.create("hide_null", true);
 318 prefs.create("fix_nonlink_styles", true);
 319 prefs.create("fix_background_image", true);
 320 prefs.create("fix_readers_guide_link", true);
 321 prefs.create("fix_readers_guide_link_style", true);
 322 prefs.create("fix_community_resources_tab_text", true);
 323 
 324 prefs.create("fix_more_search_options_title", true);
 325 prefs.create("fix_more_search_options_link", true);
 326 prefs.create("fix_more_search_options_history", true);
 327 
 328 prefs.create("fix_title", true);
 329 prefs.create("end_titles_with_lpl", true);
 330 
 331 prefs.create("fix_js_links", true);
 332 
 333 prefs.create("maintain_session", true);
 334 prefs.create("maintain_session_interval", 5000); // How often to check whether the server should be pinged.
 335 prefs.create("session_timeout_interval", 1000 * 60); // How often to ping the server; default 1 min.
 336 
 337 // We set the defaults of both of these to now() because
 338 // only the one (if any) that applies to this page will 
 339 // be accessed.
 340 prefs.create("last_catalog_access", now());
 341 prefs.create("last_cr_access", now());
 342 
 343 
 344 
 345 
 346 function get_prefs_interface() {
 347     var checkboxes = [
 348         // [pref_name, checkbox_label, long_desc], 
 349         ["change_fields", "Change field types", "Make the login form's password fields normal text fields."],
 350         ["remember_info", "Remember my info", "Remember my library card number and PIN."],
 351         ["auto_login", "Automatically log in", "Fill and submit any login form encountered."],
 352         ["hide_null", "Hide null", 'Hide the "null" on the borrower overview page.'],
 353         ["fix_nonlink_styles", "Fix the appearance of nonlink text", "Force only links to look like links"],
 354         ["fix_background_image", "Fix background image", "Stop the flowery red background from tiling on very wide monitors"],
 355         //["fix_readers_guide_link_style", 'Fix "Reader\'s Guide" link style', "Makes the \"Reader's Guide\" link on the Information Toolbox page look unvisited."],
 356         //["fix_community_resources_tab_text", "Fix Community Resources tab text", ""],
 357         ["fix_more_search_options_title", "Fix \"More Search Options\" title", "Removes the text \"Click Here For ;\" from the page title."],
 358         ["fix_more_search_options_link", "Fix \"More Search Options\" link", "Remove \"Click For\" from the link text."],
 359         ["fix_more_search_options_history", "Fix \"More Search Options\" history", "Fix \"More Search Options\" search type in the search history display."],
 360         ["fix_title", "Add page titles", "Add appropriate page titles to pages that lack them"],
 361         ["end_titles_with_lpl", "End page titles with \u201cLa Crosse Public Library\u201d", "For pages for which titles are added"],
 362         ["fix_js_links", "Fix \"javascript:\" links", "Modify JavaScript-protocol links, allowing them to be middle-clicked when possible."],
 363         ["disable_timer", "Disable timer", "Disable the client-side code that automatically logs you out after about five minutes by sending you to the logout page.  This prevents a new page being suddenly loaded without you clicking on a link or button."],
 364         ["maintain_session", "Maintain session", "Prevent the session from expiring on the server by periodically sending it requests.  This stops the server from automatically logging you out after about five minutes.\xA0 Note: this feature functions by making periodic requests to the server, which may create congestion on limited bandwith Internet links."]
 365     ];
 366     
 367     var homepage_link = El("A", "Home Page");
 368     homepage_link.href = "http://freecog.net/2006/lplfixup/";
 369     var checkbox_container = El("DIV.checkboxes");
 370     
 371     var barcode_input = El("INPUT");
 372     var pin_input = El("INPUT");
 373     barcode_input.type = pin_input.type = "text";
 374     barcode_input.size = 16; pin_input.size = 4;
 375     prefs.link_text_field("barcode", barcode_input, true);
 376     prefs.link_text_field("pin", pin_input, true);
 377     var clear_button = El("BUTTON", "Clear");
 378     clear_button.addEventListener("DOMActivate", function() {
 379         prefs.barcode = '';
 380         prefs.pin = '';
 381     }, false);
 382     var info_container = El("DIV.info",
 383         El("LABEL", "Card number: ", barcode_input),
 384         El("LABEL", "\xA0\xA0\xA0\xA0 PIN: ", pin_input),
 385         "\xA0\xA0\xA0\xA0 ", clear_button
 386     );
 387     
 388     var close_button = El("BUTTON.close_button", "Close");
 389     
 390     var container =  El("DIV#lplfixup_prefs",
 391         El("H1", "LPLFixup Options"),
 392         El("P", homepage_link),
 393         info_container,
 394         checkbox_container,
 395         El("DIV.bottombar", 
 396             close_button,
 397             El("P", "Your changes take place immediately.")
 398         )
 399     );
 400     
 401     checkboxes.forEach(function(item) {
 402         checkbox_container.appendChild(El("P.checkbox", El("LABEL", prefs.make_checkbox(item[0]), " ", item[1]), item[2]));
 403     });
 404     
 405     
 406     close_button.addEventListener("DOMActivate", function(evt) {
 407         var event = document.createEvent("Event");
 408         event.initEvent("LPLFixup_Close", false, true);
 409         container.dispatchEvent(event);
 410     }, false);
 411     
 412     return container;
 413 }
 414 
 415 
 416 // A button to forget saved info
 417 var forget_button = El("BUTTON", "Forget");
 418 if (!prefs.barcode && !prefs.pin) {
 419     forget_button.disabled = true;
 420 }
 421 forget_button.addEventListener("DOMActivate", function(evt) {
 422     prefs.barcode = '';
 423     prefs.pin = '';
 424     alert("Your barcode and PIN have been forgotten.");
 425     forget_button.disabled = true;
 426     evt.stopPropagation();
 427     evt.preventDefault();
 428 }, false);
 429 
 430 var homepage_link = El("A", "LPLFixup Home Page");
 431 homepage_link.setAttribute('href', 'http://freecog.net/2006/lplfixup/');
 432 
 433 var prefs_dialog = null;
 434 var prefs_button = El("BUTTON.prefs_button", "More Options");
 435 prefs_button.addEventListener("DOMActivate", function(evt) {
 436     if (!prefs_dialog) {
 437         prefs_dialog = get_prefs_interface();
 438     }
 439     document.body.appendChild(prefs_dialog);
 440     evt.stopPropagation();
 441     evt.preventDefault();
 442     prefs_button.disabled = true;
 443     prefs_dialog.addEventListener("LPLFixup_Close", function(evt) {
 444         document.body.removeChild(prefs_dialog);
 445         prefs_button.disabled = false;
 446     }, false);
 447 }, false);
 448 
 449 
 450 /////////////////////////////////
 451 ////// LOGIN FORM HANDLING //////
 452 /////////////////////////////////
 453 
 454 // The login form.
 455 var form = document.forms[0];
 456 // The input fields.
 457 var barcode_input, pin_input, message_container;
 458 
 459 function restore_info() {
 460     if (prefs.remember_info) {
 461         log("Restoring info");
 462         barcode_input.value = prefs.barcode;
 463         pin_input.value = prefs.pin;
 464     }
 465 }
 466 
 467 // Set the event listener to save the barcode and PIN
 468 // when the form is submitted.
 469 function set_save_listener() {
 470     form.addEventListener('submit', function() {
 471         // Save info
 472         if (prefs.remember_info) {
 473             log("Saving info");
 474             prefs.barcode = barcode_input.value;
 475             prefs.pin = pin_input.value;
 476         }
 477     }, false);
 478 }
 479 
 480 function get_message(details) {
 481     return El("P", El("STRONG", "Unable to automatically log in: " + details + "."));
 482 }
 483 
 484 const NEED_MORE_INFO = "please complete your login information";
 485 const ATTEMPT_FAILED = "detected failed attempt. Please correct your information";
 486 
 487 if (is_login_page()) { // Login form
 488     log("This is an iPac login page.");
 489     
 490     form = document.forms[0];
 491     barcode_input = form.elements.namedItem('sec1');
 492     pin_input = form.elements.namedItem('sec2');
 493     
 494     
 495     // I like to be able to see my typos, thank you.
 496     if (prefs.change_fields)
 497         barcode_input.type = pin_input.type = 'text';
 498     
 499     restore_info();
 500     
 501     // Auto-submit
 502     if (prefs.auto_login) {
 503         var details;
 504         if (!prefs.barcode || !prefs.pin) {
 505             details = NEED_MORE_INFO;
 506         } else if (login_failed()) {
 507             details = ATTEMPT_FAILED;
 508         } else {
 509             form.submit();
 510             return; // No need to continue
 511         }
 512         var msg = get_message(details);
 513         msg.style.color = 'red';
 514         var tbody = pin_input.parentNode.parentNode.parentNode;
 515         tbody.parentNode.style.width = '50%';
 516         var cell = tbody.lastChild.firstChild;
 517         cell.replaceChild(msg, cell.firstChild);
 518     }
 519     
 520     set_save_listener();
 521 
 522 } else if (is_remote_auth_page()) {
 523     log("This is a remote authentication page.");
 524     
 525     form = document.forms[0];
 526     
 527     // Is this the library selection page?
 528     if (form.elements.namedItem("lb").type != "hidden") {
 529         // Then we'll fill it and submit it.
 530         form.elements.namedItem("lb").value = "LACRS";
 531         if (prefs.auto_login) {
 532             form.submit();
 533         }
 534         return;
 535     } else {
 536         barcode_input = form.elements.namedItem('h1');
 537         pin_input = form.elements.namedItem('h2');
 538         
 539         if (prefs.change_fields)
 540             pin_input.type = 'text';
 541         
 542         restore_info();
 543     
 544         if (prefs.auto_login) {
 545             var details;
 546             if (!prefs.barcode || !prefs.pin) {
 547                 details = NEED_MORE_INFO;
 548             } else if (document.body.innerHTML.match(/Sorry, Your identification is not recognized/)) {
 549                 details = ATTEMPT_FAILED;
 550             } else {
 551                 // We can login!
 552                 form.submit();
 553                 return; // No need to continue processing the page.
 554             }
 555             var msg = get_message(details);
 556             msg.style.color = 'red';
 557             input.parentNode.appendChild(msg);
 558         }
 559         
 560         set_save_listener();
 561     }
 562 }
 563 
 564 
 565 // Set up the inline prefs.
 566 var inline_prefs = El("DIV#lplfixup_inline_prefs", 
 567     El("STRONG", "LPLFixup"),
 568     El("LABEL", prefs.make_checkbox("remember_info"), " Remember my info ", forget_button),
 569     El("LABEL", prefs.make_checkbox("auto_login"), " Automatically login"),
 570     El("LABEL", prefs.make_checkbox("disable_timer"), " Disable automatic logout"),
 571     " ", prefs_button, homepage_link
 572 );
 573 document.body.appendChild(inline_prefs);
 574 
 575 
 576 // Insert the style sheet
 577 GM_addStyle([
 578     '#lplfixup_prefs {',
 579         'font-size: .8em;',
 580         'text-align: left;',
 581         'border: 2px solid black;',
 582         'padding: 10px;',
 583         'position: absolute;',
 584         'left: 100px;',
 585         'right: 100px;',
 586         'top: 100px;',
 587         'margin-bottom: 100px;',
 588         'background: white;',
 589     '}',
 590     '#lplfixup_prefs h1 {',
 591         'margin-top: 0;',
 592         'line-height: 1;',
 593         'text-align: center;',
 594     '}',
 595     '#lplfixup_prefs div.checkboxes {',
 596         '-moz-column-count: 2;',
 597     '}',
 598     '#lplfixup_prefs p {',
 599         'text-indent: -25px;',
 600         'margin-left: 25px;',
 601     '}',
 602     '#lplfixup_prefs p label {',
 603         'display: block;',
 604         'font-size: 1.2em;',
 605     '}',
 606     '#lplfixup_prefs .info {',
 607         'font-size: 1.2em;',
 608     '}',
 609     '#lplfixup_prefs .close_button {',
 610         'float: right;',
 611     '}',
 612     '#lplfixup_prefs .bottombar {',
 613         'margin-top: 20px;',
 614     '}',
 615     '#lplfixup_prefs button,',
 616     '#lplfixup_inline_prefs button {',
 617         'font-size: inherit;',
 618         'font-family: inherit;',
 619     '}',
 620     '#lplfixup_inline_prefs label {',
 621         'white-space: nowrap;',
 622     '}',
 623     '#lplfixup_prefs a,',
 624     '#lplfixup_inline_prefs a {',
 625         'white-space: nowrap;',
 626         'margin-left: 2em;',
 627     '}',
 628     '#lplfixup_inline_prefs label {',
 629         'margin-left: 1em;',
 630     '}',
 631     '#lplfixup_inline_prefs button.prefs_button {',
 632         'margin-left: 2em;',
 633     '}',
 634     '#lplfixup_inline_prefs {',
 635         'font-size: .7em;',
 636         'text-align: center;',
 637         'margin-bottom: 10px;',
 638         'width: 80%;', // It overflows the <body> without this width declaration in narrow windows.
 639         'margin-left: auto; margin-right: auto;', // Center
 640     '}',
 641 ].join('\n'));
 642 
 643 
 644 // Set up the timer killer
 645 var orig_timer = unsafeWindow.Timer;
 646 
 647 function set_timer(value) {
 648     if (value) {
 649         log("NOOP Timer set.");
 650         unsafeWindow.location.href = "javascript:" + 
 651             encodeURIComponent("Timer = function(){}; void(0);");
 652     } else {
 653         unsafeWindow.location.href = "javascript:" + 
 654             encodeURIComponent("Timer = " + orig_timer.toSource().
 655                 replace(/closeTime/g, 'window.closeTime') + "; Timer(); void(0);"
 656             );
 657         log("Original Timer set.");
 658     }
 659 }
 660 
 661 prefs.listen("disable_timer", set_timer);
 662 set_timer(prefs.disable_timer);
 663 
 664 
 665 
 666 //////////////////////////////
 667 //  SET THE DOCUMENT TITLE  //
 668 //////////////////////////////
 669 
 670 // Titles in the two major sections of the interface are displayed
 671 // differently in each area, with added versions for the serch results
 672 // pages and the individual item pages.
 673 //
 674 // "Search the Catalog" -- search forms
 675 //    An <a class="boldBlackFont2">&nbsp;
 676 //    ``get_serach_form_title()``
 677 // "My Library Record" -- borrower information
 678 //    An <a class="big">
 679 //    ``get_myrecord_title()``
 680 // Search Results
 681 //    An <a class="boldBlackFont3">
 682 //    ``get_search_results_title()``
 683 // Item Record Page
 684 //    An <a class="largeAnchor">
 685 //    ``get_record_title()``
 686 //
 687 // All these functions return null if they don't find an appropriate
 688 // title.
 689 //
 690 // I now suspect that a better architecture might have been to figure
 691 // out which link in the navigation is highlighted--but oh well.
 692 
 693 
 694 // A helper function for finding <a>s with a particular class.
 695 // <a>s with presentational class names are used  on iPac pages much 
 696 // like <span>s might be on a site designed by a rational mind.
 697 // Fortunately, Horizon doesn't seem to have any concept of 
 698 // multiple classes, and doesn't surround them with whitespace, so
 699 // a simple equality comparison works.
 700 function get_as_by_class_name(class_name) {
 701     if (typeof(class_name) == 'string')
 702         class_name = new RegExp('(^|\s)' + class_name + '(\s|$)');
 703     return Array.slice(document.getElementsByTagName('a') || []).filter(function(a) {
 704         return a.className.match(class_name);
 705     });
 706 }
 707 
 708 // Maps the title pulled from the page content to the title
 709 // used when the title is inferred from the URL.  This is used 
 710 // instead of the title in the content where possible for consistency
 711 // with the page titles generated elsewhere.
 712 var myrecord_title_remap = {
 713     'Items Out': 'Checked Out',
 714     'Hold Requests': 'Holds',
 715     'Blocks': 'Fines/Blocks',
 716     'Profile': 'Borrower Information',
 717 };
 718 
 719 // Gets the text of the first <a> with a class of "big" -- this
 720 // is appropriate as a document title for "My Library Record" pages.
 721 function get_myrecord_title() {
 722     var big_as = get_as_by_class_name('big');
 723     if (big_as[0]) {
 724         var text = big_as[0].textContent;
 725         var title = text.replace(/\s-.*/, '');
 726         // For consistency's sake, map the title to whatever we use
 727         // when it's inferred from the URL.
 728         return myrecord_title_remap[title] || title;
 729     }
 730     return null;
 731 }
 732 
 733 // Get a document title on a "My List" listing
 734 // These listings look much like search result pages from 
 735 // a HTML perspective.
 736 function get_mylist_title() {
 737     var containers = get_as_by_class_name('boldBlackFont2');
 738     if (containers[0] && containers[0].textContent.match(/\d+\s+titles/)) {
 739         var titles = containers[0].textContent;
 740         if (titles.match(/^1\s+titles$/)) {
 741             titles = '1 title';
 742             // Might as well fix this, while we're at it.
 743             containers[0].firstChild.nodeValue = titles;
 744         }
 745         var list_name = containers[0].parentNode.parentNode.previousSibling.firstChild.textContent;
 746         return list_name + " (" + titles + ")";
 747     } else {
 748         log("No containers.");
 749     }
 750     
 751     // New list name prompt
 752     if (document.forms && document.forms.namedItem("ipac") && 
 753         document.forms.namedItem("ipac").elements.namedItem("moveto_listkey") && 
 754         document.forms.namedItem("ipac").elements.namedItem("moveto_listkey").value == "ipac_new_saved_list") {
 755         return "Enter new list name";
 756     }
 757     
 758     log("Not a \"My List\" listing.");
 759     return null;
 760 }
 761 
 762 
 763 function get_mylist_management_title() {
 764     // We find this page by the names of the table columns.
 765     if (document.body.textContent.match(/ListsListTitlesCreatedExpires/)) {
 766         return 'Manage Lists';
 767     }
 768     return null;
 769 }
 770 
 771 // Like `myrecorrd_title_remap`, but for search form pages.
 772 var search_form_title_remap = {
 773     'BASIC SEARCHES': 'Basic Search',
 774     'Click Here For ;More Search Options': 'More Search Options',
 775 };
 776 
 777 // Get a document title on a search form page.
 778 function get_search_form_title() {
 779     var containers = get_as_by_class_name('boldBlackFont2');
 780     if (containers[0]) {
 781         var a = containers[0];
 782         var text = a.textContent.replace(/^[\xA0\s]+|\s+$/g, "");
 783         return search_form_title_remap[text] || text;
 784     }
 785     return null;
 786 }
 787 
 788 // Get a document title on a search results page.
 789 function get_search_result_title() {
 790     var bbf3_as = get_as_by_class_name('boldBlackFont3');
 791     if (bbf3_as[0]) {
 792         var a1 = bbf3_as[0];
 793         var bolded = a1.parentNode.parentNode.nextSibling.getElementsByTagName('b');
 794         if (bolded.length == 1) {
 795             // Just the query
 796             var info = bolded[0].textContent;
 797         } else if (bolded.length == 2) {
 798             // Include the number of results
 799             var info = bolded[1].textContent + " (" + bolded[0].textContent + ")";
 800         }
 801         return a1.textContent.replace(/^\s+/, '') + ": " + info
 802     }
 803     return null;
 804 }
 805 
 806 // Get a document title for an item record page.
 807 function get_record_title() {
 808     var anchors = get_as_by_class_name('largeAnchor');
 809     if (anchors[0]) {
 810         return anchors[0].textContent.replace(/^\s+|\s+$/, '');
 811     }
 812 }
 813 
 814 // Get the document title for record in the Community Resources
 815 // database.
 816 function get_community_resources_record_title() {
 817     // These records lack any prominent header--they're just flat DB fields.
 818     // All record pages seem to contain the text "Question/Headline:", so
 819     // that'll be used to detect them.
 820     if (document.body.textContent.indexOf("Question/Headline:") > -1) {
 821         // The text relevant to the title is contained in the <a class="normalBlackFont1">
 822         // after the one containing "Question/Headline: ".  This seems to be the third one, 
 823         // but I'm not going to count on it.
 824         var containers = get_as_by_class_name("normalBlackFont1");
 825         var extract_next = false; // set to true when "Question/Headline" is encountered.
 826         for (var i = 0; i < containers.length; i++) {
 827             if (extract_next) {
 828                 return containers[i].textContent.replace(/^\s+|\s+$/g, ""); // Should ellipsize this?
 829             } else if (containers[i].textContent.match(/Question\/Headline:/i)) {
 830                 extract_next = true;
 831             }
 832         }
 833         
 834         // Nothing was extracted.  Fallback:
 835         log("Unable to extract a title for this Community Resources page.");
 836         return "Item Record";
 837         
 838         // The fallback will also be displayed for MARC records.  I'm not going to bother
 839         // changing that unless someone requests it.
 840     }
 841     return null;
 842 }
 843 
 844 
 845 // Get a document title for the Information Toolbox page.
 846 function get_information_toolbox_title() {
 847     // Is this the information toolbox page?  Since it has 
 848     // no content, we find out by examining the navigation.
 849     var pseudo_spans = get_as_by_class_name("TabActive");
 850     if (pseudo_spans[0] && pseudo_spans[0].title == "Great databases") {
 851         return "Information Toolbox";
 852     } else {
 853         return null;
 854     }
 855 }
 856 
 857 
 858 // Dict mapping the URL components 'menu' and 'submenu' to page titles.
 859 var url_to_title = {
 860     'search': {
 861         DEFAULT: 'Basic Search',
 862         'basic_search': 'Basic Search',
 863         'advanced': 'More Search Options',
 864         'power': 'Power Search',
 865         'history': 'Search History'
 866     },
 867     'account': {
 868         DEFAULT: 'Account Overview',
 869         'overview': 'Account Overview',
 870         'itemsout': 'Checked Out',
 871         'holds': 'Holds',
 872         'blocks': 'Fines/Blocks',
 873         'info': 'Borrower Information'
 874     },
 875     'Information%20Resources': {
 876         DEFAULT: 'Information Toolbox'
 877     },
 878     'mylist': {
 879         DEFAULT: 'My List'
 880     }
 881 };
 882 
 883 
 884 // Infer from the URL -- this would be used in the
 885 // general case, except it doesn't work on POST or login pages.
 886 function get_title_from_url() {
 887     var url = document.location.href;
 888     // There must be a querystring, and it can't be one for a logout page
 889     if (!url.match(/\/ipac.jsp$|[\?&]logout=true(&|$)/)) {
 890         try {
 891             var menu = url.match(/[?&]menu=([\w\d%]+)(&|$)/)[1];
 892             var submenu = 'DEFAULT';
 893             try {
 894                 var submenu = url.match(/[?&]submenu=(\w+)(&|$)/)[1];
 895             } catch(e) { /* go with DEFAULT */ }
 896             return url_to_title[menu][submenu] || url_to_title[menu].DEFAULT;
 897         } catch(e) {
 898             log("Error parsing the querystring to get a title: " + e);
 899             log("  menu = " + menu);
 900             log("  submenu = " + submenu);
 901         }
 902     }
 903     log("Not able to parse the querystring for a title.");
 904     return null;
 905 };
 906 
 907 // Get the title if this is a login page.
 908 function get_login_title() {
 909     return is_login_page() ? 'Login' : null;
 910 };
 911 
 912 var title_getters = [
 913     get_login_title,
 914     get_mylist_title,
 915     get_mylist_management_title,
 916     get_search_result_title,
 917     get_record_title,
 918     get_community_resources_record_title,
 919     get_title_from_url,
 920     // Fallbacks
 921     get_information_toolbox_title,
 922     get_search_form_title,
 923     get_myrecord_title,
 924 ];
 925 
 926 // Code poetry:
 927 function fix_title() {
 928     // Does the document need a title?
 929     var need = (!document.title || document.title === "iPac2.0")
 930     // Can it be fixed?
 931     var ability = document.location.href.match(/\/ipac.jsp?/);
 932     return need && ability;
 933 }
 934 
 935 if (prefs.fix_title && fix_title()) {
 936     // Kill me now ;-)
 937     var title, i = 0;
 938     while (title_getters[i] && !(title = title_getters[i]())) { i++; }
 939     
 940     if (!title) {
 941         // Fallback, if nothing else is found
 942         title = 'Online Catalog';
 943         log("No title found \u2014 a fallback will be used.");
 944     }
 945     
 946     if (prefs.end_titles_with_lpl) {
 947         title += ' \u2014 La Crosse Public Library';
 948     }
 949     
 950     document.title = title;
 951     log("document.title set to \u201c" + title + "\u201d")
 952 }
 953 
 954 
 955 
 956 ////////////////////////////////////////////
 957 //  HIDE THAT 'null' ON THE ACCOUNT PAGE  //
 958 ////////////////////////////////////////////
 959 
 960 if (prefs.hide_null) {
 961     var ps = document.getElementsByTagName('p');
 962     if (ps && ps[1] && ps[1].textContent == 'null') {
 963             ps[1].parentNode.removeChild(ps[1]);
 964     }
 965 }
 966 
 967 //////////////////////////////////////////
 968 //  MAKE NON-LINKS NOT LOOK LIKE LINKS  //
 969 //////////////////////////////////////////
 970 
 971 if (prefs.fix_nonlink_styles && is_ipac_page) {
 972     GM_addStyle([
 973         'a[class$=Anchor]:not([href]) {',
 974             'font-weight: normal;',
 975             'color: black;',
 976         '}',
 977         'a[class$=Anchor]:not([href]):hover {',
 978             'text-decoration: none;',
 979         '}',
 980         'a[class$=Anchor]:not([href]):active {',
 981             'font-style: normal;',
 982         '}'
 983     ].join('\n'));
 984 }
 985 
 986 /////////////////////////////////////////////////////////////////////
 987 //  REMOVE "Click Here For ;" FROM THE "More Search Options" PAGE  //
 988 /////////////////////////////////////////////////////////////////////
 989 
 990 if (prefs.fix_more_search_options_title && is_ipac_page) {
 991     var a_tags = get_as_by_class_name("boldBlackFont2");
 992     if (a_tags.length) {
 993         var a = a_tags[0];
 994         if (a.textContent.match(/Click Here For ;More Search Options/)) {
 995             a.firstChild.textContent = "\xA0More Search Options";
 996             log('"More Search Options" page title corrected');
 997         }
 998     }
 999 }
 1000 
 1001 ////////////////////////////////
 1002 //  FIX THE BACKGROUND IMAGE  //
 1003 ////////////////////////////////
 1004 
 1005 if (prefs.fix_background_image && !is_ipac_page) {
 1006     GM_addStyle([
 1007         'body[background$="redbackground.gif"] {',
 1008             'background: white url(/images/redbackground.gif) repeat-y top left;',
 1009         '}'
 1010     ].join('\n'));
 1011     log("Any red-background issue has been corrected.");
 1012 }
 1013 
 1014 
 1015 //////////////////////////////
 1016 //  FIX READER'S GUIDE LINK //
 1017 //////////////////////////////
 1018 
 1019 if (prefs.fix_readers_guide_link || prefs.fix_readers_guide_link_style) {
 1020     get_as_by_class_name('navBarCurrent').forEach(function(a) {
 1021         if (a.textContent == "Readers' Guide") {
 1022             if (prefs.fix_readers_guide_link && a.href == "http://lplcat.lacrosse.lib.wi.us/rpa/webauth.exe?rs=RG") {
 1023                 a.href += '&lb=LACRS';
 1024             }
 1025             if (prefs.fix_readers_guide_link_style) {
 1026                 a.className = 'navBarAnchor';
 1027             }
 1028         }
 1029     });
 1030 }
 1031 
 1032 ////////////////////////////////////////////
 1033 //  FIX THE COMMUNITY RESOURCES TAB TEXT  //
 1034 ////////////////////////////////////////////
 1035 
 1036 if (prefs.fix_community_resources_tab_text) {
 1037     get_as_by_class_name('TabActive').forEach(function(a) {
 1038         if (a.textContent == "Local news index&Fast Facts")
 1039             a.firstChild.nodeValue = 'Local news index & Fast Facts';
 1040     });
 1041 }
 1042 
 1043 /////////////////////////////////////////
 1044 //  FIX MORE SEARCH OPTIONS LINK TEXT  //
 1045 /////////////////////////////////////////
 1046 
 1047 // Remove "Click For" from the "Click For More Search Options" link.
 1048 if (prefs.fix_more_search_options_link) {
 1049     get_as_by_class_name('navBarAnchor|navBarCurrent').forEach(function(a) {
 1050         if (a.textContent == "Click ForMore Search Options") {
 1051             a.innerHTML = "More Search Options";
 1052             log('Fixed "More Search Options" link text.');
 1053         }
 1054     });
 1055 }
 1056 
 1057 /////////////////////////////////////////////////////////////
 1058 //  FIX "Click For;More Search Options" IN SEARCH HISTORY  //
 1059 /////////////////////////////////////////////////////////////
 1060 
 1061 if (prefs.fix_more_search_options_history) {
 1062     // Is this a search history page? -- &menu=search&submenu=history
 1063     var url = document.location.href;
 1064     if (url.match(/[\?&]menu=search(&|$)/) && url.match(/[\?&]submenu=history(&|$)/)) {
 1065         get_as_by_class_name('normalBlackFont1').forEach(function(a) {
 1066             if (a.textContent == "Click For;More Search Options") {
 1067                 a.firstChild.nodeValue = "More Search Options";
 1068             }   
 1069         });
 1070         log('Done fixing the search history page\'s "More Search Options" entries.');
 1071     }
 1072 }
 1073 
 1074 
 1075 ////////////////////////////
 1076 //  FIX JAVASCRIPT LINKS  //
 1077 ////////////////////////////
 1078 
 1079 // Function reference:
 1080 /*
 1081 
 1082 buildReturnPageNewList(url, return_url)
 1083     Submits the form "buildLink", which has just one field, named
 1084     "returnURL," which is filled with the `return_url` parameter.
 1085     The action of the form is set to `url`, and the form is POST'ed.
 1086     (Note that the url is unescape()'d when navigator.appName ==
 1087     'Netscape', too.)
 1088         Replacement action: simply use `url` as the href of the link.
 1089     This may break the "Return to results" link, but who cares?
 1090     That's what the back button's for.
 1091 ReturnSearchPage(url)
 1092     Submits either the form "SearchResult" or the form "buildLink" 
 1093     with `url` as the action, or falls back to setting document.location
 1094     to `url`.  `url` is unescape()'d.
 1095         Replacement action: href_from_string_arg()
 1096 buildNewList(url, return_url, summary)
 1097     I'm not exactly sure what this is trying to to.  I guess that 
 1098     it is accumulating a breadcrum trail of history.  That's pointless,
 1099     since it's already implemeneted in the form of the back button, and
 1100     will likely break when multiple tabs are used anyway.  Axed.
 1101         Replacement action: href_from_string_arg()
 1102 loginIntoOrOutOfAccount(url, return_url)
 1103     This seems to combine buildReturnPageNewList and buildNewList.
 1104         Replacement action: href_from_string_arg()
 1105 AddCopy(bkey, ikey, pos)
 1106     This is part of the booklist system.  It's pure JavaScript, so it
 1107     shouldn't be replaced.
 1108 buildMyList(url, return_url)
 1109     Yet another thing that seems to do whatever buildReturnPageNewList
 1110     and buildNewList do.
 1111         Replacement action: href_from_string_arg()
 1112 
 1113 */
 1114 
 1115 // Extracts the body of a string literal as if it had been eval()'d.
 1116 // This is probably painfully slow.
 1117 function eval_string(string) {
 1118     var quote = string[0];
 1119     if (!(quote == "'" || quote == '"') || string[string.length-1] != quote || string.length < 2) 
 1120         throw new Error("This doesn't look like a string literal: " + string);
 1121     string = string.slice(1, -1);
 1122     var chunks = string.split(/(\\(?:['"\/bfnrt]|u[a-zA-Z0-9]{4}|U[a-zA-Z0-9]{8}))/);
 1123     for (var i = 0; i < chunks.length; i++) {
 1124         if (chunks[i][0] == '\\') {
 1125             chunks[i] = eval(quote + chunks[i] + quote);
 1126         }
 1127     }
 1128     return chunks.join('');
 1129 }
 1130 
 1131 // Extracts and sets `a`'s href propery to the the text of the first 
 1132 // single-quoted string in the href.
 1133 function href_from_string_arg(a) {
 1134     // The string extraction RE isn't right, but the URLs never seem 
 1135     // to contain escaped apostrophes anyway.
 1136     a.href = unescape(eval_string(a.href.match(/'(\\'|[^'])+'/)[0]));
 1137 }
 1138 
 1139 function fix_popUpHelp(a) {
 1140     a.setAttribute('onclick', a.href.replace(/javascript:/, "") + 'return false;');
 1141     href_from_string_arg(a);
 1142 }
 1143 
 1144 // Maps link function names to functions that set a valid HREF.
 1145 // NOOP functions are provided where it is not possible to provide
 1146 // a non-JavaScript equivalent.
 1147 var link_functions_to_fixes = {
 1148     buildReturnPageNewList:  href_from_string_arg,
 1149     ReturnSearchPage:        href_from_string_arg,
 1150     viewlargeimage:          function(){}, // NOOP
 1151     AddCopy:                 function(){}, // NOOP
 1152     buildNewList:            href_from_string_arg,
 1153     buildMyList:             href_from_string_arg,
 1154     popUpHelp:               fix_popUpHelp,
 1155     loginIntoOrOutOfAccount: href_from_string_arg,
 1156 };
 1157 
 1158 if (prefs.fix_js_links) {
 1159     // Dispatch to the functions for link types.
 1160     Array.slice(document.getElementsByTagName("a")).filter(function(a) {
 1161         return ( a.href && a.href.match(/^\s*javascript:/i) );
 1162     }).forEach(function(a) {
 1163         var func_name = a.href.match(/^\s*javascript:\s*([a-z0-9_$]+)/i)[1];
 1164         var func = link_functions_to_fixes[func_name];
 1165         if (func) {
 1166             func(a, func_name);
 1167             if (DEBUG) {
 1168                 if (a.href.match(/^http:/i)) {
 1169                     a.href += "#fixed_from_" + func_name;
 1170                 } else {
 1171                     a.href += "//fixed from " + func_name;
 1172                 }
 1173             }
 1174         } else {
 1175             log("Unable to fix a link calling " + func_name);
 1176             log("  link was " + a.href);
 1177         }
 1178     });
 1179     log("Link fixing completed");
 1180 }
 1181 
 1182 
 1183 ///////////////////////////
 1184 // MAINTAIN THE SESSION  //
 1185 ///////////////////////////
 1186 
 1187 
 1188 function is_community_resources_page() {
 1189     return document.location.href.match(/^https?:\/\/lpl.lacrosse.lib.wi.us:81\//i);
 1190 }
 1191 
 1192 var session_interval_ident = null;
 1193 var access_pref_name = is_community_resources_page() ? "last_cr_access" : "last_catalog_access";
 1194 
 1195 function maintain_session() {
 1196     if (!prefs.maintain_session) {
 1197         session_interval_ident = null;
 1198         return;
 1199     }
 1200     
 1201     var expires_at = prefs[access_pref_name] + prefs.session_timeout_interval;
 1202     if (expires_at <= now()) {
 1203         // Pull the url to load out of the navigation (we need one that
 1204         // contains the session token).
 1205         var link_found = false;
 1206         var links = get_as_by_class_name("TabInactive|navBarAnchor");
 1207         for (var i = 0; i < links.length; i++) {
 1208             if (links[i].href.match(/^https?:\/\/lpl.lacrosse.lib.wi.us/)) {
 1209                 GM_xmlhttpRequest({
 1210                     method: 'GET',
 1211                     url: links[i].href,
 1212                     onload: function() {
 1213                         log("Session extension request completed.");
 1214                         prefs[access_pref_name] = now();
 1215                     },
 1216                     onerror: function() {
 1217                         log("Error encountered when making the session extension request.");
 1218                     }
 1219                 });
 1220                 link_found = true;
 1221                 break;
 1222             }
 1223         }
 1224         
 1225         if (link_found) {
 1226             log("Request sent to extend the session.");
 1227         } else {
 1228             log("Couldn't find a link to extend the session with");
 1229         }   
 1230     }
 1231     
 1232     session_interval_ident = window.setTimeout(maintain_session, prefs.maintain_session_interval);
 1233 }
 1234 
 1235 if (is_ipac_page) {
 1236     log("Starting session maintenance loop.");
 1237     prefs[access_pref_name] = now();
 1238     maintain_session();
 1239     prefs.listen("maintain_session", function(value, old_value, name) {
 1240         if (value && !old_value) maintain_session();
 1241         if (!value && session_value_ident) window.clearTimeout(session_value_ident);
 1242     });
 1243 }
 1244 
 1245 })();