LP#1434728: make password_required configurable per library
[sitka/overdrive-evergreen-opac.git] / src / od_api.coffee
CommitLineData
8ebc3fff 1define [
b63fc3d5 2 'jquery-noconflict'
8ebc3fff
SC
3 'lodash'
4 'json'
5 'cookies'
6 'moment'
7 'od_config'
bf67304d 8 'od_session'
9669700c
SC
9 'od_data'
10], ($, _, json, C, M, config, Session, D) ->
8ebc3fff
SC
11
12 # Dump the given arguments or log them to console
13 log = ->
14 try
15 dump "#{x}\n" for x in arguments
16 return
17 catch
18 console.log arguments
19 return
20
21 $notify = $ {}
22
23 logError = (jqXHR, textStatus, errorThrown) ->
24 log "#{textStatus} #{jqXHR.status} #{errorThrown}"
25 $notify.trigger 'od.fail', arguments
26
27 # Define custom event names for this module. A custom event is triggered
28 # whenever result data becomes available after making an API request.
29 eventList = [
30 'od.clientaccess'
9669700c 31 'od.libraryinfo'
8ebc3fff
SC
32 'od.metadata'
33 'od.availability'
34
35 'od.patronaccess'
36 'od.patroninfo'
37 'od.holds'
38 'od.checkouts'
39 'od.interests'
40 'od.action'
41
42 'od.hold.update'
43 'od.hold.delete'
44 'od.checkout.update'
45 'od.checkout.delete'
46
47 'od.prefs'
48 'od.login'
49 'od.logout'
50 'od.error'
51 ]
63192819
SC
52 eventObject = $({}).on eventList.join(' '), (e, x, y...) ->
53 # Uncomment for debugging on console
54 #log e.namespace, x, y
8ebc3fff 55
bf67304d
SC
56 # On page load, we unserialize the text string found in local storage into
57 # an object, or if there is no string yet, we create the default object.
8ebc3fff
SC
58 # The session object uses a local storage mechanism based on window.name;
59 # see
60 # http://stackoverflow.com/questions/2035075/using-window-name-as-a-local-data-cache-in-web-browsers
61 # for pros and cons and alternatives.
bf67304d 62 session = new Session window.name, Boolean C('eg_loggedin') or window.IAMXUL
8ebc3fff 63
bf67304d
SC
64 # On window unload, we serialize it into local storage so that it survives
65 # page reloads.
66 $(window).on 'unload', -> window.name = session.store()
8ebc3fff
SC
67
68 # Customize the plain jQuery ajax to post a request for an access token
69 _api = (url, data) ->
70
71 $.ajax $.extend {},
72 # The Basic Authorization string is always added to the HTTP header.
73 headers: Authorization: "Basic #{config.credentials}"
508513a1 74 url: url
8ebc3fff
SC
75 type: 'POST'
76 # We expect data to be always given; the ajax method will convert
77 # it to a query string.
78 data: data
79
8ebc3fff
SC
80 # Convert a serialized array into a serialized object
81 serializeObject = (a) ->
82 o = {}
83 $.each a, ->
84 v = @value or ''
85 if (n = o[@name]) isnt undefined
86 o[@name] = [n] unless n.push
87 o[@name].push v
88 else
89 o[@name] = v
90 return o
91
92 # TODO unused
93 $.fn.extend
94
95 # Convert this serialized array to a serialized object
96 _serializeObject: -> serializeObject @serializeArray()
97
98 # Serialize this to a json string, an object, an array, a query string, or just return itself
99 _serializeX: (X) ->
100 switch X
101 when 'j' then json.stringify @_serializeX 'o'
102 when 'k' then json.stringify @_serializeX 'a'
103 when 'p' then $.param @_serializeX 'a'
104 when 's' then @serialize()
105 when 'o' then serializeObject @_serializeX 'a'
106 when 'a' then @serializeArray()
107 else @
108
8ebc3fff
SC
109
110 # We define the public interface of the module
111 # TODO wrap od in jquery so that we can use it to trigger events and bind event handlers
112 od =
113
114 # Povides the anchor object for implementing a publish/subscribe
115 # mechanism for this module.
116 $: eventObject.on
117
118 # Notification that there are possible changes of values from
119 # preferences page that should be updated in the session cache
bf67304d 120 'od.prefs': (ev, x) -> session.prefs.update x
8ebc3fff
SC
121
122 # Expire patron access token if user is no longer logged into EG
123 'od.logout': (ev, x) ->
124 if x is 'eg'
bf67304d 125 session = new Session() if session.token.is_patron_access()
8ebc3fff
SC
126
127 log: log
128
8ebc3fff
SC
129 # Map format id to format name using current session object
130 labels: (id) -> session.labels[id] or id
131
132 # Customize the plain jQuery ajax method to handle a GET or POST method
133 # for the Overdrive api.
8546cdf5 134 api: (url, method, data, beforeSend) ->
8ebc3fff
SC
135
136 # Do some pre-processing of data before it is sent to server
137 if method is 'post'
138
139 # Convert numberOfDays value from an ISO 8601 date string to
140 # number of days relative to now. There are two subtleties
141 # regarding rounding errors: First, we use only use now of
142 # resolution to days to avoid a local round-down from 1 to 0.
143 # Second, we need to add one to avoid a round-down at the OD
144 # server.
145 for v in data.fields when v.name is 'numberOfDays'
146 v.value = 1 + M(v.value).diff M().toArray()[0..2], 'days'
147
148 $.ajax $.extend {},
149 # The current Authorization string is always added to the HTTP header.
bf67304d 150 headers: Authorization: "#{session.token.token_type} #{session.token.access_token}"
508513a1 151 url: url
8ebc3fff
SC
152 # Will default to 'get' if no method string is supplied
153 type: method
154 # A given data object is expected to be in JSON format
155 contentType: 'application/json; charset=utf-8'
156 data: json.stringify data
8546cdf5 157 beforeSend: beforeSend
8ebc3fff
SC
158
159 .done ->
160
161 # For a post method, we get a data object in reply. We publish
162 # the object using an event named after the data type, eg,
163 # 'hold', 'checkout'. We can't easily recognize the data type
164 # by looking at the data, so we have to pattern match on the
165 # API URL.
166 if method is 'post'
167 if /\/holds|\/suspension/.test url
9669700c 168 x = new D.Holds holds: [ arguments[0] ]
8ebc3fff
SC
169 od.$.triggerHandler 'od.hold.update', x
170 if /\/checkouts/.test url
9669700c 171 x = new D.Checkouts checkouts: [ arguments[0] ]
8ebc3fff
SC
172 od.$.triggerHandler 'od.checkout.update', x
173
174 # For a delete method, we do not get a data object in reply,
9669700c 175 # thus we pattern match for the specific ID, and trigger an
8ebc3fff
SC
176 # event with the ID.
177 if method is 'delete'
178 if id = url.match /\/holds\/(.+)\/suspension$/
179 return # no relevant event
180 if id = url.match /\/holds\/(.+)$/
9669700c 181 od.$.triggerHandler 'od.hold.delete', id[1]
8ebc3fff 182 if id = url.match /\/checkouts\/(.+)$/
9669700c 183 od.$.triggerHandler 'od.checkout.delete', id[1]
8ebc3fff
SC
184
185 .fail ->
186 od.$.triggerHandler 'od.error', [url, arguments[0]]
187 #$('<div>')._notify arguments[1].statusText, arguments[0].responseText
188
9669700c
SC
189 # Get a library access token so that we can use the Discovery API.
190 # The token is cached and also published to other modules.
8ebc3fff
SC
191 apiDiscAccess: ->
192
193 ok = (x) ->
bf67304d 194 session.token.update x
8ebc3fff 195 od.$.triggerHandler 'od.clientaccess', x
9669700c 196 return x
8ebc3fff
SC
197
198 _api session.links.token.href, grant_type: 'client_credentials'
199
200 .then ok, logError
201
202 # Use the Library Account API to get library account information,
203 # primarily the product link and the available formats. Since we
204 # schedule this call on every page load, it will also tell us if
205 # our access token has expired or not.
206 #
207 # If a retry is needed, we have to decide whether to get a library
208 # access token or a patron access token. However, getting the latter
209 # will, in the general case, require user credentials, which means we
210 # need to store the password in the browser across sessions. An
211 # alternative is to force a logout, so that the user needs to manually
212 # relogin. In effect, we would only proceed with a retry to get a
213 # library access token, but if the user has logged in, we would not.
214 #
9669700c 215 apiLibraryInfo: ->
8ebc3fff
SC
216
217 get = -> od.api session.links.libraries.href
218
219 ok = (x) ->
bf67304d
SC
220 session.links.update x
221 session.labels.update x
9669700c
SC
222 od.$.triggerHandler 'od.libraryinfo', x
223 return x
8ebc3fff
SC
224
225 retry = (jqXHR) ->
226
227 # Retry if we got a 401 error code
228 if jqXHR.status is 401
229
bf67304d 230 if session.token.is_patron_access()
8ebc3fff
SC
231 # Current OD patron access token may have expired
232 od.$.triggerHandler 'od.logout', 'od'
233
234 else
235 # Renew our access token and retry the get operation
236 od.apiDiscAccess()
237 .then get, logError
238 .then ok
239
240 get().then ok, retry
241
242 # We define a two-phase sequence to get a patron access token, for example,
243 # login(credentials); do_something_involving_page_reload(); login();
244 # where credentials is an object containing username and password
245 # properties from the login form.
246 #
247 # Logging into Evergreen can proceed using either barcode or user name,
248 # but logging into Overdrive is only a natural act using barcode. In
249 # order to ensure that logging into OD with a username can proceed, we
250 # presume that EG has been logged into and, as a prelude, we get the
251 # Preferences page of the user so that we can scrape out the barcode
252 # value for logging into OD.
253 #
254 # The login sequence is associated with a cache that remembers the
255 # login response ('parameters') between login sessions. The epilogue to
256 # the login sequence is to use the Patron Information API to get URL
257 # links and templates that will allow the user to make further use of
258 # the Circulation API.
259 login: (credentials) ->
260
261 # Temporarily store the username and password from the login form
bf67304d 262 # into the session cache, and invalidate the session token so that
8ebc3fff
SC
263 # the final part of login sequence can complete.
264 if credentials
bf67304d
SC
265 session.creds.update credentials
266 session.token.update()
8ebc3fff
SC
267 od.$.triggerHandler 'od.login'
268 return
269
bf67304d 270 # Return a promise to a resolved deferredment if session token is still valid
8ebc3fff 271 # TODO is true if in staff client but shouldn't be
bf67304d 272 if session.token.is_patron_access()
8ebc3fff
SC
273 return $.Deferred().resolve().promise()
274
275 # Request OD service for a patron access token using credentials
276 # pulled from the patron's preferences page
277 login = (prefs) ->
278
279 # Define a function to cut the value corresponding to a label
280 # from prefs
281 x = (label) ->
282 r = new RegExp "#{label}<\\/td>\\s+<td.+>(.*?)<\\/td>", 'i'
283 prefs.match(r)?[1] or ''
284
285 # Retrieve values from preferences page and save them in the
286 # session cache for later reference
bf67304d 287 session.prefs.update
8ebc3fff
SC
288 barcode: x 'barcode'
289 email_address: x 'email address'
290 home_library: x 'home library'
8ebc3fff
SC
291
292 # Use barcode as username or the username that was stored in
293 # session cache (in the hope that is a barcode) or give up with
294 # a null string
bf67304d 295 un = session.prefs.barcode or session.creds.un()
8ebc3fff
SC
296
297 # Use the password that was stored in session cache or a dummy value
27e91a35 298 pw = session.creds.pw config.password_required(session.prefs.home_library)
8ebc3fff
SC
299
300 # Remove the stored credentials from cache as soon as they are
301 # no longer needed
bf67304d 302 session.creds.update()
8ebc3fff
SC
303
304 # Determine the Open Auth scope by mapping the long name of EG
305 # home library to OD authorization name
306 scope = "websiteid:#{config.websiteID} authorizationname:#{config.authorizationname session.prefs.home_library}"
307
308 # Try to get a patron access token from OD server
309 _api session.links.patrontoken.href,
310 grant_type: 'password'
311 username: un
312 password: pw
27e91a35 313 password_required: config.password_required session.prefs.home_library
8ebc3fff
SC
314 scope: scope
315
316 # Complete login sequence if the session cache is invalid
317 ok = (x) ->
bf67304d 318 session.token.update x
8ebc3fff 319 od.$.triggerHandler 'od.patronaccess', x
9669700c 320 return x
8ebc3fff
SC
321
322 # Get patron preferences page
323 $.get '/eg/opac/myopac/prefs'
324 # Get the access token using credentials from preferences
325 .then login
326 # Update the session cache with access token
327 .then ok
328 # Update the session cache with session links
329 .then od.apiPatronInfo
330 .fail log
331
332 # TODO not used; EG catalogue is used instead
333 apiSearch: (x) ->
334 return unless x
335 od.api session.links.products.href, get, x
336
8ebc3fff
SC
337 apiMetadata: (x) ->
338 return unless x.id
9669700c 339
8ebc3fff
SC
340 od.api "#{session.links.products.href}/#{x.id}/metadata"
341
342 .then (y) ->
9669700c 343 y = new D.Metadata y
8ebc3fff
SC
344 od.$.triggerHandler 'od.metadata', y
345 y
346
347 .fail -> od.$.triggerHandler 'od.metadata', x
348
349 apiAvailability: (x) ->
350 return unless x.id
351
352 url =
353 if (alink = session.links.availability?.href)
354 alink.replace '{crId}', x.id # Use this link if logged in
355 else
356 "#{session.links.products.href}/#{x.id}/availability"
357
358 od.api url
359
8ebc3fff 360 .then (y) ->
9669700c 361 y = new D.Availability y, session.prefs.email_address
8ebc3fff 362 od.$.triggerHandler 'od.availability', y
9669700c 363 return y
8ebc3fff
SC
364
365 .fail -> od.$.triggerHandler 'od.availability', x
366
367 apiPatronInfo: ->
9669700c 368
8ebc3fff 369 ok = (x) ->
bf67304d 370 session.links.update x
8ebc3fff 371 od.$.triggerHandler 'od.patroninfo', x
9669700c 372 return x
8ebc3fff
SC
373
374 od.api session.links.patrons.href
375 .then ok, logError
376
9669700c 377 # Get a specific hold or all holds
8ebc3fff 378 apiHoldsGet: (x) ->
bf67304d 379 return unless session.token.is_patron_access()
8ebc3fff
SC
380
381 od.api "#{session.links.holds.href}#{if x?.productID then x.productID else ''}"
382
8ebc3fff 383 .then (y) ->
9669700c 384 y = new D.Holds y
8ebc3fff 385 od.$.triggerHandler 'od.holds', y
9669700c 386 return y
8ebc3fff 387
9669700c 388 # Get a specific checkout or all checkouts
8ebc3fff 389 apiCheckoutsGet: (x) ->
bf67304d 390 return unless session.token.is_patron_access()
8ebc3fff
SC
391
392 od.api "#{session.links.checkouts.href}#{if x?.reserveID then x.reserveID else ''}"
393
8ebc3fff 394 .then (y) ->
9669700c 395 y = new D.Checkouts y
8ebc3fff 396 od.$.triggerHandler 'od.checkouts', y
9669700c 397 return y
8ebc3fff 398
9669700c
SC
399 # Consolidate the holds and checkouts lists into an object that
400 # represents the 'interests' of the patron
8ebc3fff
SC
401 apiInterestsGet: ->
402 $.when(
403 od.apiHoldsGet()
404 od.apiCheckoutsGet()
405 )
8ebc3fff 406 .then (h, c) ->
9669700c
SC
407 y = new D.Interests h, c
408 od.$.triggerHandler 'od.interests', y
409 return y
8ebc3fff
SC
410
411 return od