def increment_error_count(key): """ :param str key: e.g. ol.exceptions or el.internal-errors-segmented """ top_url_path = 'none' page_class = 'none' if web.ctx and hasattr(web.ctx, 'path') and web.ctx.path: top_url_path = _get_top_level_path_for_metric(web.ctx.path) page_class = _get_path_page_name(web.ctx.path) exception_type, exception_value, tback = sys.exc_info() exception_type_name = exception_type.__name__ # Log exception file path = find_topmost_useful_file(exception_value, tback) path = os.path.split(path) # log just filename, unless it's code.py (cause that's useless!) ol_file = path[1] if path[1] in ('code.py', 'index.html', 'edit.html', 'view.html'): ol_file = os.path.split(path[0])[1] + '_' + _encode_key_part(path[1]) metric_parts = [ top_url_path, 'class_' + page_class, ol_file, exception_type_name, 'count', ] metric = '.'.join([_encode_key_part(p) for p in metric_parts]) graphite_stats.increment(key + '.' + metric)
def POST_leave_waitinglist(self, edition, user, i): waitinglist.leave_waitinglist(user.key, edition.key) stats.increment('ol.loans.leaveWaitlist') if i.get("redirect"): raise web.redirect(i.redirect) else: raise web.redirect(edition.url())
def POST(self): form = web.input() email = form.get("email", "") topic = form.get("topic", "") description = form.get("question", "") url = form.get("url", "") user = accounts.get_current_user() useragent = web.ctx.env.get("HTTP_USER_AGENT","") if not all([email, topic, description]): return "" default_assignees = config.get("support_default_assignees",{}) topic_key = str(topic.replace(" ","_").lower()) if topic_key in default_assignees: assignee = default_assignees.get(topic_key) else: assignee = default_assignees.get("default", "*****@*****.**") stats.increment("ol.support.all") subject = "Support case *%s*"%topic url = web.ctx.home + url displayname = user and user.get_name() or "" username = user and user.get_username() or "" message = SUPPORT_EMAIL_TEMPLATE % locals() sendmail(email, assignee, subject, message) return render_template("email/case_created", assignee)
def POST(self): form = web.input() email = form.get("email", "") topic = form.get("topic", "") description = form.get("question", "") url = form.get("url", "") user = accounts.get_current_user() useragent = web.ctx.env.get("HTTP_USER_AGENT", "") if not all([email, topic, description]): return "" default_assignees = config.get("support_default_assignees", {}) topic_key = str(topic.replace(" ", "_").lower()) if topic_key in default_assignees: assignee = default_assignees.get(topic_key) else: assignee = default_assignees.get("default", "*****@*****.**") stats.increment("ol.support.all") subject = "Support case *%s*" % topic url = web.ctx.home + url displayname = user and user.get_name() or "" username = user and user.get_username() or "" message = SUPPORT_EMAIL_TEMPLATE % locals() sendmail(email, assignee, subject, message) return render_template("email/case_created", assignee)
def unlink(self): """Careful, this will save any other changes to the ol user object as well """ _ol_account = web.ctx.site.store.get(self._key) _ol_account['internetarchive_itemname'] = None web.ctx.site.store[self._key] = _ol_account self.internetarchive_itemname = None stats.increment('ol.account.xauth.unlinked')
def link(self, itemname): """Careful, this will save any other changes to the ol user object as well """ itemname = itemname if itemname.startswith('@') else '@%s' % itemname _ol_account = web.ctx.site.store.get(self._key) _ol_account['internetarchive_itemname'] = itemname web.ctx.site.store[self._key] = _ol_account self.internetarchive_itemname = itemname stats.increment('ol.account.xauth.linked')
def setup(): "Basic startup status for the server" global status_info, feature_flags host = socket.gethostname() status_info = { "Software version": get_software_version(), "Python version": sys.version.split()[0], "Host": host, "Start time": datetime.datetime.utcnow(), } feature_flags = get_features_enabled() # Host is e.g. ol-web4.blah.archive.org ; we just want the first subdomain first_subdomain = host.split('.')[0] or 'unknown' stats.increment('ol.servers.%s.started' % first_subdomain)
def POST(self): # if not support_db: # return "Couldn't initialise connection to support database" form = web.input() email = form.get("email", "") topic = form.get("topic", "") description = form.get("question", "") url = form.get("url", "") user = accounts.get_current_user() useragent = web.ctx.env.get("HTTP_USER_AGENT", "") if not all([email, topic, description]): return "" default_assignees = config.get("support_default_assignees", {}) topic_key = str(topic.replace(" ", "_").lower()) if topic_key in default_assignees: # This is set to False to prevent cases from being created # even if there is a designated assignee. This prevents # the database from being updated. create_case = False assignee = default_assignees.get(topic_key) else: create_case = False assignee = default_assignees.get("default", "*****@*****.**") if create_case: c = support_db.create_case( creator_name=user and user.get_name() or "", creator_email=email, creator_useragent=useragent, creator_username=user and user.get_username() or "", subject=topic, description=description, url=url, assignee=assignee, ) stats.increment("support.all") else: stats.increment("support.all") subject = "Support case *%s*" % topic url = web.ctx.home + url displayname = user and user.get_name() or "" username = user and user.get_username() or "" message = SUPPORT_EMAIL_TEMPLATE % locals() sendmail(email, assignee, subject, message) return render_template("email/case_created", assignee)
def POST(self): form = web.input() patron_name = form.get("name", "") email = form.get("email", "") topic = form.get("topic", "") description = form.get("question", "") url = form.get("url", "") user = accounts.get_current_user() useragent = web.ctx.env.get("HTTP_USER_AGENT", "") if not all([email, topic, description]): return "" hashed_ip = hashlib.md5(web.ctx.ip.encode('utf-8')).hexdigest() has_emailed_recently = get_memcache().get('contact-POST-%s' % hashed_ip) if has_emailed_recently: recap = get_recaptcha() if recap and not recap.validate(): return render_template( "message.html", 'Recaptcha solution was incorrect', ('Please <a href="javascript:history.back()">go back</a> and try ' 'again.'), ) default_assignees = config.get("support_default_assignees", {}) topic_key = str(topic.replace(" ", "_").lower()) if topic_key in default_assignees: assignee = default_assignees.get(topic_key) else: assignee = default_assignees.get("default", "*****@*****.**") stats.increment("ol.support.all") subject = "Support case *%s*" % topic url = web.ctx.home + url displayname = user and user.get_name() or "" username = user and user.get_username() or "" message = SUPPORT_EMAIL_TEMPLATE % locals() sendmail(email, assignee, subject, message) get_memcache().set('contact-POST-%s' % hashed_ip, "true", time=15 * MINUTE_SECS) return render_template("email/case_created", assignee)
def POST(self): # if not support_db: # return "Couldn't initialise connection to support database" form = web.input() email = form.get("email", "") topic = form.get("topic", "") description = form.get("question", "") url = form.get("url", "") user = accounts.get_current_user() useragent = web.ctx.env.get("HTTP_USER_AGENT", "") if not all([email, topic, description]): return "" default_assignees = config.get("support_default_assignees", {}) topic_key = str(topic.replace(" ", "_").lower()) if topic_key in default_assignees: # This is set to False to prevent cases from being created # even if there is a designated assignee. This prevents # the database from being updated. create_case = False assignee = default_assignees.get(topic_key) else: create_case = False assignee = default_assignees.get("default", "*****@*****.**") if create_case: c = support_db.create_case( creator_name=user and user.get_name() or "", creator_email=email, creator_useragent=useragent, creator_username=user and user.get_username() or "", subject=topic, description=description, url=url, assignee=assignee) stats.increment("support.all") else: stats.increment("support.all") subject = "Support case *%s*" % topic message = "A new support case has been filed\n\nTopic: %s\n\nDescription:\n%s" % ( topic, description) web.sendmail(email, assignee, subject, message) return render_template("email/case_created", assignee)
def POST(self): if not support_db: return "Couldn't initialise connection to support database" form = web.input() email = form.get("email", "") topic = form.get("topic", "") description = form.get("question", "") url = form.get("url", "") user = accounts.get_current_user() useragent = web.ctx.env.get("HTTP_USER_AGENT","") if not all([email, topic, description]): return "" default_assignees = config.get("support_default_assignees",{}) topic_key = topic.replace(" ","_").lower() if topic_key in default_assignees: create_case = True assignee = default_assignees.get(topic_key) else: create_case = False assignee = default_assignees.get("default", "*****@*****.**") if create_case: c = support_db.create_case(creator_name = user and user.get_name() or "", creator_email = email, creator_useragent = useragent, creator_username = user and user.get_username() or "", subject = topic, description = description, url = url, assignee = assignee) stats.increment("support.all") else: stats.increment("support.all") subject = "Support case *%s*"%topic message = "A new support case has been filed\n\nTopic: %s\n\nDescription:\n%s"%(topic, description) web.sendmail(email, assignee, subject, message) return render_template("email/case_created", assignee)
def stats_hook(): """web.py unload hook to add X-OL-Stats header. This info can be written to lighttpd access log for collecting Also, send stats to graphite using statsd """ stats_summary = stats.stats_summary() update_all_stats(stats_summary) try: if "stats-header" in web.ctx.features: web.header("X-OL-Stats", format_stats(stats_summary)) except Exception as e: # don't let errors in stats collection break the app. print(str(e), file=web.debug) # This name is misleading. It gets incremented for more than just pages. # E.g. *.json requests (even ajax), image requests. Although I can't # see any *.js requests? So not sure exactly when we're called here. graphite_stats.increment('ol.pageviews') memcache_hits = 0 memcache_misses = 0 for s in web.ctx.get("stats", []): if s.name == 'memcache.get': if s.data['hit']: memcache_hits += 1 else: memcache_misses += 1 if memcache_hits: graphite_stats.increment('ol.memcache.hits', memcache_hits, rate=0.025) if memcache_misses: graphite_stats.increment('ol.memcache.misses', memcache_misses, rate=0.025) for name, value in stats_summary.items(): name = name.replace(".", "_") time = value.get("time", 0.0) * 1000 key = 'ol.' + name graphite_stats.put(key, time)
def audit_accounts(email, password, require_link=False, s3_access_key=None, s3_secret_key=None, test=False): """Performs an audit of the IA or OL account having this email. The audit: - verifies the password is correct for this account - aborts if any sort of error (e.g. account blocked, unverified) - reports whether the account is linked (to a secondary account) - if unlinked, reports whether a secondary account exists w/ matching email Args: email (unicode) password (unicode) require_link (bool) - if True, returns `accounts_not_connected` if accounts are not linked test (bool) - not currently used; is there to allow testing in the absence of archive.org dependency """ from openlibrary.core import lending if s3_access_key and s3_secret_key: r = InternetArchiveAccount.s3auth(s3_access_key, s3_secret_key) if not r.get('authorized', False): return {'error': 'invalid_s3keys'} ia_login = "******" email = r['username'] else: if not valid_email(email): return {'error': 'invalid_email'} ia_login = InternetArchiveAccount.authenticate(email, password) if any(ia_login == err for err in ['account_blocked', 'account_locked']): return {'error': 'account_locked'} if ia_login != "ok": # Prioritize returning other errors over `account_not_found` if ia_login != "account_not_found": return {'error': ia_login} return {'error': 'account_not_found'} else: ia_account = InternetArchiveAccount.get(email=email, test=test) # Get the OL account which links to this IA account ol_account = OpenLibraryAccount.get(link=ia_account.itemname, test=test) link = ol_account.itemname if ol_account else None # The fact that there is no link implies no Open Library # account exists containing a link to this Internet Archive # account... if not link: # then check if there's an Open Library account which shares # the same email as this IA account. ol_account = OpenLibraryAccount.get(email=email, test=test) # If an Open Library account with a matching email account exist... if ol_account: # Check whether it is linked already, i.e. has an itemname # set. We already determined that no OL account is # linked to our IA account. Therefore this Open # Library account having the same email as our IA # account must have been linked to a different # Internet Archive account. if ol_account.itemname: return {'error': 'wrong_ia_account'} # At this point, it must either be the case that (a) # `ol_account` already links to our IA account (in which case # `link` has a correct value), (b) that an unlinked # `ol_account` shares the same email as our IA account and # thus can and should be safely linked to our IA account, or # (c) no `ol_account` which is linked or can be linked has # been found and therefore, assuming # lending.config_ia_auth_only is enabled, we need to create # and link it. if not ol_account: if not password: raise {'error': 'link_attempt_requires_password'} try: ol_account = OpenLibraryAccount.create( ia_account.itemname, email, password, displayname=ia_account.screenname, verified=True, retries=5, test=test) except ValueError as e: return {'error': 'max_retries_exceeded'} ol_account.link(ia_account.itemname) stats.increment('ol.account.xauth.ia-auto-created-ol') # So long as there's either a linked OL account, or an unlinked OL # account with the same email, set them as linked (and let the # finalize logic link them, if needed) else: if not ol_account.itemname: ol_account.link(ia_account.itemname) stats.increment('ol.account.xauth.auto-linked') if not ol_account.verified: # The IA account is activated (verifying the # integrity of their email), so we make a judgement # call to safely activate them. ol_account.activate() if ol_account.blocked: return {'error': 'account_blocked'} if require_link: ol_account = OpenLibraryAccount.get(link=ia_account.itemname, test=test) if ol_account and not ol_account.itemname: return {'error': 'accounts_not_connected'} # When a user logs in with OL credentials, the # web.ctx.site.login() is called with their OL user # credentials, which internally sets an auth_token # enabling the user's session. The web.ctx.site.login # method requires OL credentials which are not present in # the case where a user logs in with their IA # credentials. As a result, when users login with their # valid IA credentials, the following kludge allows us to # fetch the OL account linked to their IA account, bypass # this web.ctx.site.login method (which requires OL # credentials), and directly set an auth_token to # enable the user's session. web.ctx.conn.set_auth_token(ol_account.generate_login_code()) return { 'authenticated': True, 'ia_email': ia_account.email, 'ol_email': ol_account.email, 'ia_username': ia_account.screenname, 'ol_username': ol_account.username, 'link': ol_account.itemname, }
def POST_join_waitinglist(self, edition, user): waitinglist.join_waitinglist(user.key, edition.key) stats.increment('ol.loans.joinWaitlist') raise web.redirect(edition.url())
def POST(self, key): """Called when the user wants to borrow the edition""" i = web.input(action='borrow', format=None, ol_host=None) if i.ol_host: ol_host = i.ol_host else: ol_host = 'openlibrary.org' edition = web.ctx.site.get(key) if not edition: raise web.notfound() # Make a call to availability v2 update the subjects according # to result if `open`, redirect to bookreader response = lending.get_availability_of_ocaid(edition.ocaid) availability = response[edition.ocaid] if response else {} if availability and availability['status'] == 'open': raise web.seeother('https://archive.org/stream/' + edition.ocaid + '?ref=ol') error_redirect = ('https://archive.org/stream/' + edition.ocaid + '?ref=ol') user = accounts.get_current_user() if user: account = OpenLibraryAccount.get_by_email(user.email) ia_itemname = account.itemname if account else None if not user or not ia_itemname: web.setcookie(config.login_cookie_name, "", expires=-1) raise web.seeother("/account/login?redirect=%s/borrow?action=%s" % (edition.url(), i.action)) action = i.action # Intercept a 'borrow' action if the user has already # borrowed the book and convert to a 'read' action. # Added so that direct bookreader links being routed through # here can use a single action of 'borrow', regardless of # whether the book has been checked out or not. if action == 'borrow' and user.has_borrowed(edition): action = 'read' if action == 'borrow': resource_type = i.format or 'bookreader' if resource_type not in ['epub', 'pdf', 'bookreader']: raise web.seeother(error_redirect) user_meets_borrow_criteria = user_can_borrow_edition( user, edition, resource_type) if user_meets_borrow_criteria: # This must be called before the loan is initiated, # otherwise the user's waitlist status will be cleared # upon loan creation track_loan = False if is_users_turn_to_borrow( user, edition) else True loan = lending.create_loan(identifier=edition.ocaid, resource_type=resource_type, user_key=ia_itemname, book_key=key) if loan: loan_link = loan['loan_link'] if resource_type == 'bookreader': if track_loan: # As of 2017-12-14, Petabox will be # responsible for tracking borrows which # are the result of waitlist redemptions, # so we don't want to track them here to # avoid double accounting. When a reader # is at the head of a waitlist and goes to # claim their loan, Petabox now checks # whether the waitlist was initiated from # OL, and if it was, petabox tracks # ol.loans.bookreader accordingly via # lending.create_loan. stats.increment('ol.loans.bookreader') raise web.seeother( make_bookreader_auth_link( loan.get_key(), edition.ocaid, '/stream/' + edition.ocaid, ol_host)) elif resource_type == 'pdf': stats.increment('ol.loans.pdf') raise web.seeother(loan_link) elif resource_type == 'epub': stats.increment('ol.loans.epub') raise web.seeother(loan_link) else: raise web.seeother(error_redirect) else: raise web.seeother(error_redirect) elif action == 'return': # Check that this user has the loan user.update_loan_status() loans = get_loans(user) # We pick the first loan that the user has for this book that is returnable. # Assumes a user can't borrow multiple formats (resource_type) of the same book. user_loan = None for loan in loans: # Handle the case of multiple edition records for the same # ocaid and the user borrowed from one and returning from another has_loan = (loan['book'] == edition.key or loan['ocaid'] == edition.ocaid) if has_loan: user_loan = loan break if not user_loan: # $$$ add error message raise web.seeother(error_redirect) user_loan.return_loan() # Show the page with "you've returned this". Use a dummy slug. # $$$ this would do better in a session variable that can be cleared # after the message is shown once raise web.seeother(edition.url()) elif action == 'read': # Look for loans for this book user.update_loan_status() loans = get_loans(user) for loan in loans: if loan['book'] == edition.key: raise web.seeother( make_bookreader_auth_link(loan['_key'], edition.ocaid, '/stream/' + edition.ocaid, ol_host)) elif action == 'join-waitinglist': return self.POST_join_waitinglist(edition, user) elif action == 'leave-waitinglist': return self.POST_leave_waitinglist(edition, user, i) # Action not recognized raise web.seeother(error_redirect)
def POST(self, key): """Called when the user wants to borrow the edition""" i = web.input(action='borrow', format=None, ol_host=None) if i.ol_host: ol_host = i.ol_host else: ol_host = 'openlibrary.org' edition = web.ctx.site.get(key) if not edition: raise web.notfound() error_redirect = edition.url("/borrow") user = accounts.get_current_user() if not user: raise web.seeother(error_redirect) if i.action == 'borrow': resource_type = i.format if resource_type not in ['epub', 'pdf', 'bookreader']: raise web.seeother(error_redirect) if resource_type == 'bookreader': #ab.convert("borrow-layout") pass if user_can_borrow_edition(user, edition, resource_type): # XXX-Anand - Sept 2014: # Extra check to avoid book being given to someone when people are already # waiting on it. There are some reports of this happening. # Redirect back to the same page if there is waitinglist. wl = edition.get_waitinglist() if wl and (wl[0].get_user_key() != user.key or wl[0]['status'] != 'available'): raise web.seeother(error_redirect) loan = lending.create_loan( identifier=edition.ocaid, resource_type=resource_type, user_key=user.key, book_key=key) loan_link = loan['loan_link'] if resource_type == 'bookreader': stats.increment('ol.loans.bookreader') elif resource_type == 'pdf': stats.increment('ol.loans.pdf') elif resource_type == 'epub': stats.increment('ol.loans.epub') if resource_type == 'bookreader': raise web.seeother(make_bookreader_auth_link(loan.get_key(), edition.ocaid, '/stream/' + edition.ocaid, ol_host)) else: raise web.seeother(loan_link) else: # Send to the borrow page raise web.seeother(error_redirect) elif i.action == 'return': # Check that this user has the loan user.update_loan_status() loans = get_loans(user) # We pick the first loan that the user has for this book that is returnable. # Assumes a user can't borrow multiple formats (resource_type) of the same book. user_loan = None for loan in loans: # Handle the case of multiple edition records for the same # ocaid and the user borrowed from one and returning from another has_loan = (loan['book'] == edition.key or loan['ocaid'] == edition.ocaid) if has_loan and can_return_resource_type(loan['resource_type']): user_loan = loan break if not user_loan: # $$$ add error message raise web.seeother(error_redirect) user_loan.return_loan() # Show the page with "you've returned this" # $$$ this would do better in a session variable that can be cleared # after the message is shown once raise web.seeother(edition.url('/borrow?r=t')) elif i.action == 'read': # Look for loans for this book user.update_loan_status() loans = get_loans(user) for loan in loans: if loan['book'] == edition.key: raise web.seeother(make_bookreader_auth_link(loan['_key'], edition.ocaid, '/stream/' + edition.ocaid, ol_host)) elif i.action == 'join-waitinglist': return self.POST_join_waitinglist(edition, user) elif i.action == 'leave-waitinglist': return self.POST_leave_waitinglist(edition, user, i) # Action not recognized raise web.seeother(error_redirect)
def POST(self, key): """Called when the user wants to borrow the edition""" i = web.input(action='borrow', format=None, ol_host=None) if i.ol_host: ol_host = i.ol_host else: ol_host = 'openlibrary.org' edition = web.ctx.site.get(key) if not edition: raise web.notfound() error_redirect = edition.url("/borrow") user = accounts.get_current_user() if not user: raise web.seeother(error_redirect) if i.action == 'borrow': resource_type = i.format if resource_type not in ['epub', 'pdf', 'bookreader']: raise web.seeother(error_redirect) if resource_type == 'bookreader': ab.convert("borrow-layout") if user_can_borrow_edition(user, edition, resource_type): loan = Loan(user.key, key, resource_type) if resource_type == 'bookreader': # The loan expiry should be utc isoformat loan.expiry = datetime.datetime.utcfromtimestamp( time.time() + bookreader_loan_seconds).isoformat() loan_link = loan.make_offer( ) # generate the link and record that loan offer occurred # $$$ Record fact that user has done a borrow - how do I write into user? do I need permissions? # if not user.has_borrowed: # user.has_borrowed = True # user.save() if resource_type == 'bookreader': stats.increment('loans.bookreader') elif resource_type == 'pdf': stats.increment('loans.pdf') elif resource_type == 'epub': stats.increment('loans.epub') if resource_type == 'bookreader': raise web.seeother( make_bookreader_auth_link(loan.get_key(), edition.ocaid, '/stream/' + edition.ocaid, ol_host)) else: raise web.seeother(loan_link) else: # Send to the borrow page raise web.seeother(error_redirect) elif i.action == 'return': # Check that this user has the loan user.update_loan_status() loans = get_loans(user) # We pick the first loan that the user has for this book that is returnable. # Assumes a user can't borrow multiple formats (resource_type) of the same book. user_loan = None for loan in loans: # Handle the case of multiple edition records for the same # ocaid and the user borrowed from one and returning from another has_loan = (loan['book'] == edition.key or loan['ocaid'] == edition.ocaid) if has_loan and can_return_resource_type( loan['resource_type']): user_loan = loan break if not user_loan: # $$$ add error message raise web.seeother(error_redirect) # They have it -- return it return_resource(user_loan['resource_id']) # Show the page with "you've returned this" # $$$ this would do better in a session variable that can be cleared # after the message is shown once raise web.seeother(edition.url('/borrow?r=t')) elif i.action == 'read': # Look for loans for this book user.update_loan_status() loans = get_loans(user) for loan in loans: if loan['book'] == edition.key: raise web.seeother( make_bookreader_auth_link(loan['_key'], edition.ocaid, '/stream/' + edition.ocaid, ol_host)) elif i.action == 'join-waitinglist': return self.POST_join_waitinglist(edition, user) elif i.action == 'leave-waitinglist': return self.POST_leave_waitinglist(edition, user, i) # Action not recognized raise web.seeother(error_redirect)
def POST(self, key): """Called when the user wants to borrow the edition""" i = web.input(action='borrow', format=None, ol_host=None) if i.ol_host: ol_host = i.ol_host else: ol_host = 'openlibrary.org' edition = web.ctx.site.get(key) if not edition: raise web.notfound() error_redirect = edition.url("/borrow") user = accounts.get_current_user() if not user: raise web.seeother(error_redirect) if i.action == 'borrow': resource_type = i.format if resource_type not in ['epub', 'pdf', 'bookreader']: raise web.seeother(error_redirect) if user_can_borrow_edition(user, edition, resource_type): loan = Loan(user.key, key, resource_type) if resource_type == 'bookreader': # The loan expiry should be utc isoformat loan.expiry = datetime.datetime.utcfromtimestamp(time.time() + bookreader_loan_seconds).isoformat() loan_link = loan.make_offer() # generate the link and record that loan offer occurred # $$$ Record fact that user has done a borrow - how do I write into user? do I need permissions? # if not user.has_borrowed: # user.has_borrowed = True # user.save() if resource_type == 'bookreader': stats.increment('loans.bookreader') elif resource_type == 'pdf': stats.increment('loans.pdf') elif resource_type == 'epub': stats.increment('loans.epub') if resource_type == 'bookreader': raise web.seeother(make_bookreader_auth_link(loan.get_key(), edition.ocaid, '/stream/' + edition.ocaid, ol_host)) else: raise web.seeother(loan_link) else: # Send to the borrow page raise web.seeother(error_redirect) elif i.action == 'return': # Check that this user has the loan user.update_loan_status() loans = get_loans(user) # We pick the first loan that the user has for this book that is returnable. # Assumes a user can't borrow multiple formats (resource_type) of the same book. user_loan = None for loan in loans: # Handle the case of multiple edition records for the same # ocaid and the user borrowed from one and returning from another has_loan = (loan['book'] == edition.key or loan['ocaid'] == edition.ocaid) if has_loan and can_return_resource_type(loan['resource_type']): user_loan = loan break if not user_loan: # $$$ add error message raise web.seeother(error_redirect) # They have it -- return it return_resource(user_loan['resource_id']) # Show the page with "you've returned this" # $$$ this would do better in a session variable that can be cleared # after the message is shown once raise web.seeother(edition.url('/borrow?r=t')) elif i.action == 'read': # Look for loans for this book user.update_loan_status() loans = get_loans(user) for loan in loans: if loan['book'] == edition.key: raise web.seeother(make_bookreader_auth_link(loan['_key'], edition.ocaid, '/stream/' + edition.ocaid, ol_host)) elif i.action == 'join-waitinglist': return self.POST_join_waitinglist(edition, user) elif i.action == 'leave-waitinglist': return self.POST_leave_waitinglist(edition, user, i) # Action not recognized raise web.seeother(error_redirect)
def page_unload_hook(): web.ctx.graphiteRequestStats.request_unloaded() graphite_stats.increment(web.ctx.graphiteRequestStats.to_metric())
def audit_accounts(email, password, test=False): """Performs an audit of the IA or OL account having this email. The audit: - verifies the password is correct for this account - aborts if any sort of error (e.g. account blocked, unverified) - reports whether the account is linked (to a secondary account) - if unlinked, reports whether a secondary account exists w/ matching email Args: email (unicode) password (unicode) test (bool) - not currently used; is there to allow testing in the absence of archive.org dependency """ if not valid_email(email): return {'error': 'invalid_email'} audit = { 'authenticated': False, 'has_ia': False, 'has_ol': False, 'link': False } ol_login = OpenLibraryAccount.authenticate(email, password) ia_login = InternetArchiveAccount.authenticate(email, password) if any([ err in (ol_login, ia_login) for err in ['account_blocked', 'account_locked'] ]): return {'error': 'account_locked'} # One of the accounts must authenticate w/o error if "ok" not in (ol_login, ia_login): for resp in (ol_login, ia_login): if resp != "account_not_found": return {'error': resp} return {'error': 'account_not_found'} elif ia_login == "ok": ia_account = InternetArchiveAccount.get(email=email, test=test) audit['authenticated'] = 'ia' audit['has_ia'] = email # Get the OL account which links to this IA account, if # one exists (i.e. this IA account could be linked to an # OL account with a different email) ol_account = OpenLibraryAccount.get(link=ia_account.itemname, test=test) link = ol_account.itemname if ol_account else None # If no linked account was found but there's an ol_account # having the same email as this IA account, mark them to # be linked if not link: ol_account = OpenLibraryAccount.get(email=email, test=test) link = ia_account.itemname if ol_account else None # So long as there's either a linked OL account, or an OL # account with the same email, set them as linked (and let the # finalize logic link them, if necessary) if ol_account: if not ol_account.verified: return {'error': 'account_not_verified'} if ol_account.blocked: return {'error': 'account_blocked'} audit['link'] = link audit['has_ol'] = ol_account.email # When a user logs in with OL credentials, the # web.ctx.site.login() is called with their OL user # credentials, which internally sets an auth_token # enabling the user's session. The web.ctx.site.login # method requires OL credentials which are not present in # the case where a user logs in with their IA # credentials. As a result, when users login with their # valid IA credentials, the following kludge allows us to # fetch the OL account linked to their IA account, bypass # this web.ctx.site.login method (which requires OL # credentials), and directly set an auth_token to # enable the user's session. web.ctx.conn.set_auth_token(ol_account.generate_login_code()) elif ol_login == "ok": ol_account = OpenLibraryAccount.get(email=email, test=test) audit['authenticated'] = 'ol' audit['has_ol'] = email ia_account = InternetArchiveAccount.get(email=email, test=test) # Should get the IA account linked to this OL account, if one # exists. However, xauthn API doesn't yet support `info` by # itemname. Fortunately, we have all the info we need at this # stage for the client to be satisfied. if ol_account.itemname: audit['has_ia'] = ol_account.itemname # XXX should be email audit['link'] = ol_account.itemname return audit # special case; unable to get ia acc # If the OL account is not linked but there exists an IA # account having the same email, elif ia_account: if not ia_account.verified: return {'error': 'account_not_verified'} if ia_account.locked: return {'error': 'account_blocked'} audit['has_ia'] = ia_account.itemname audit['link'] = ia_account.itemname # Links the accounts if they can be and are not already: if (audit['has_ia'] and audit['has_ol'] and audit['authenticated']): ol_account = OpenLibraryAccount.get(email=audit['has_ol'], test=test) if not ol_account.itemname: ol_account.link(audit['link']) stats.increment('ol.account.xauth.auto-linked') return audit
def POST(self, key): """Called when the user wants to borrow the edition""" i = web.input(action='borrow', format=None, ol_host=None) if i.ol_host: ol_host = i.ol_host else: ol_host = 'openlibrary.org' edition = web.ctx.site.get(key) if not edition: raise web.notfound() # Make a call to availability v2 update the subjects according # to result if `open`, redirect to bookreader response = lending.get_availability_of_ocaid(edition.ocaid) availability = response[edition.ocaid] if response else {} if availability and availability['status'] == 'open': raise web.seeother('https://archive.org/stream/' + edition.ocaid + '?ref=ol') error_redirect = ('https://archive.org/stream/' + edition.ocaid + '?ref=ol') user = accounts.get_current_user() if user: account = OpenLibraryAccount.get_by_email(user.email) ia_itemname = account.itemname if account else None if not user or not ia_itemname: web.setcookie(config.login_cookie_name, "", expires=-1) raise web.seeother("/account/login?redirect=%s/borrow?action=%s" % ( edition.url(), i.action)) action = i.action # Intercept a 'borrow' action if the user has already # borrowed the book and convert to a 'read' action. # Added so that direct bookreader links being routed through # here can use a single action of 'borrow', regardless of # whether the book has been checked out or not. if action == 'borrow' and user.has_borrowed(edition): action = 'read' if action == 'borrow': resource_type = i.format or 'bookreader' if resource_type not in ['epub', 'pdf', 'bookreader']: raise web.seeother(error_redirect) user_meets_borrow_criteria = user_can_borrow_edition(user, edition, resource_type) if user_meets_borrow_criteria: # This must be called before the loan is initiated, # otherwise the user's waitlist status will be cleared # upon loan creation track_loan = False if is_users_turn_to_borrow(user, edition) else True loan = lending.create_loan( identifier=edition.ocaid, resource_type=resource_type, user_key=ia_itemname, book_key=key) if loan: loan_link = loan['loan_link'] if resource_type == 'bookreader': if track_loan: # As of 2017-12-14, Petabox will be # responsible for tracking borrows which # are the result of waitlist redemptions, # so we don't want to track them here to # avoid double accounting. When a reader # is at the head of a waitlist and goes to # claim their loan, Petabox now checks # whether the waitlist was initiated from # OL, and if it was, petabox tracks # ol.loans.bookreader accordingly via # lending.create_loan. stats.increment('ol.loans.bookreader') raise web.seeother(make_bookreader_auth_link( loan.get_key(), edition.ocaid, '/stream/' + edition.ocaid, ol_host, ia_userid=ia_itemname)) elif resource_type == 'pdf': stats.increment('ol.loans.pdf') raise web.seeother(loan_link) elif resource_type == 'epub': stats.increment('ol.loans.epub') raise web.seeother(loan_link) else: raise web.seeother(error_redirect) else: raise web.seeother(error_redirect) elif action == 'return': # Check that this user has the loan user.update_loan_status() loans = get_loans(user) # We pick the first loan that the user has for this book that is returnable. # Assumes a user can't borrow multiple formats (resource_type) of the same book. user_loan = None for loan in loans: # Handle the case of multiple edition records for the same # ocaid and the user borrowed from one and returning from another has_loan = (loan['book'] == edition.key or loan['ocaid'] == edition.ocaid) if has_loan: user_loan = loan break if not user_loan: # $$$ add error message raise web.seeother(error_redirect) user_loan.return_loan() # Show the page with "you've returned this". Use a dummy slug. # $$$ this would do better in a session variable that can be cleared # after the message is shown once raise web.seeother(edition.url()) elif action == 'read': # Look for loans for this book user.update_loan_status() loans = get_loans(user) for loan in loans: if loan['book'] == edition.key: raise web.seeother(make_bookreader_auth_link( loan['_key'], edition.ocaid, '/stream/' + edition.ocaid, ol_host, ia_userid=ia_itemname )) elif action == 'join-waitinglist': return self.POST_join_waitinglist(edition, user) elif action == 'leave-waitinglist': return self.POST_leave_waitinglist(edition, user, i) # Action not recognized raise web.seeother(error_redirect)
def create_accounts(email, password, username="", test=False): """Retrieves the IA or OL account having correct email and password credentials """ retries = 0 if test else 10 audit = audit_accounts(email, password) if 'error' in audit or (audit['link'] and audit['authenticated']): return audit ia_account = (InternetArchiveAccount.get( email=audit['has_ia']) if audit.get('has_ia', False) else None) ol_account = (OpenLibraryAccount.get( email=audit['has_ol']) if audit.get('has_ol', False) else None) # Make sure at least one account exists # XXX should never get here, move to audit if ia_account and ol_account: if not audit['link']: if ia_account.locked or ol_account.blocked(): return {'error': 'account_blocked'} audit['link'] = ia_account.itemname ol_account.link(ia_account.itemname) stats.increment('ol.account.xauth.auto-linked') return audit elif not (ia_account or ol_account): return {'error': 'account_not_found'} # Create and link new account if email and password: if ol_account: try: ol_account_username = (username or ol_account.displayname or ol_account.username) ia_account = InternetArchiveAccount.create(ol_account_username, email, password, retries=retries, verified=True, test=test) audit['link'] = ia_account.itemname audit['has_ia'] = ia_account.email audit['has_ol'] = ol_account.email if not test: ol_account.link(ia_account.itemname) stats.increment('ol.account.xauth.ol-created-ia') return audit except ValueError as e: return {'error': 'max_retries_exceeded', 'msg': str(e)} elif ia_account: try: # always take screen name ia_account_screenname = ia_account.screenname ia_account_itemname = ia_account.itemname ol_account = OpenLibraryAccount.create( ia_account_itemname, email, password, displayname=ia_account_screenname, retries=retries, verified=True, test=test) audit['has_ol'] = ol_account.email audit['has_ia'] = ia_account.email audit['link'] = ia_account.itemname if not test: ol_account.link(ia_account.itemname) stats.increment('ol.account.xauth.ia-created-ol') return audit except ValueError as e: return {'error': 'max_retries_exceeded', 'msg': str(e)} return {'error': 'account_not_found'} return {'error': 'missing_fields'}
def link_accounts(email, password, bridgeEmail="", bridgePassword="", test=False): """Takes the correct email and password for an archive.org or openlibrary.org account and then links this account to an existing account for the complimentary service (using the bridge credentials) Args: email (unicode) - the email of the user's primary account password (unicode) - the password for the user's primary account bridgeEmail (unicode) - the email of the secondary account which to connect bridgePassword (unicode) - the password of the secondary account to connect test (bool) - isn't currently used in the link_accounts case because linking is a safely reversable operation. Test *is* used in the create_accounts case (where it prevents real accounts from being created) """ audit = audit_accounts(email, password) if 'error' in audit or (audit['link'] and audit['authenticated']): return audit ia_account = (InternetArchiveAccount.get( email=audit['has_ia']) if audit.get('has_ia', False) else None) ol_account = (OpenLibraryAccount.get( email=audit['has_ol']) if audit.get('has_ol', False) else None) # Make sure at least one account exists # XXX should never get here, move to audit if ia_account and ol_account: if not audit['link']: if ia_account.locked or ol_account.blocked(): return {'error': 'account_blocked'} audit['link'] = ia_account.itemname ol_account.link(ia_account.itemname) if not test: stats.increment('ol.account.xauth.auto-linked') return audit elif not (ia_account or ol_account): return {'error': 'account_not_found'} # Link existing accounts if bridgeEmail and bridgePassword: if not valid_email(bridgeEmail): return {'error': 'invalid_bridgeEmail'} if ol_account: _res = InternetArchiveAccount.authenticate(email=bridgeEmail, password=bridgePassword, test=test) if _res == "ok": ia_account = InternetArchiveAccount.get(email=bridgeEmail, test=test) if OpenLibraryAccount.get_by_link(ia_account.itemname): return {'error': 'account_already_linked'} ol_account.link(ia_account.itemname) audit['link'] = ia_account.itemname audit['has_ia'] = ia_account.email audit['has_ol'] = ol_account.email if not test: stats.increment('ol.account.xauth.ol-existing-ia') return audit return {'error': _res} elif ia_account: _resp = OpenLibraryAccount.authenticate(bridgeEmail, bridgePassword) if _resp == "ok": ol_account = OpenLibraryAccount.get(email=bridgeEmail, test=test) if ol_account.itemname: return {'error': 'account_already_linked'} audit['has_ia'] = ia_account.email audit['has_ol'] = ol_account.email audit['link'] = ia_account.itemname ol_account.link(ia_account.itemname) if not test: stats.increment('ol.account.xauth.ia-existing-ol') return audit return {'error': _resp} return {'error': 'account_not_found'} return {'error': 'missing_fields'}
def POST(self, key): """Called when the user wants to borrow the edition""" i = web.input(action='borrow', format=None, ol_host=None, _autoReadAloud=None, q="") ol_host = i.ol_host or 'openlibrary.org' action = i.action edition = web.ctx.site.get(key) if not edition: raise web.notfound() archive_url = get_bookreader_stream_url(edition.ocaid) + '?ref=ol' if i._autoReadAloud is not None: archive_url += '&_autoReadAloud=show' if i.q: _q = urllib.parse.quote(i.q, safe='') archive_url += "#page/-/mode/2up/search/%s" % _q # Make a call to availability v2 update the subjects according # to result if `open`, redirect to bookreader response = lending.get_availability_of_ocaid(edition.ocaid) availability = response[edition.ocaid] if response else {} if availability and availability['status'] == 'open': raise web.seeother(archive_url) error_redirect = (archive_url) user = accounts.get_current_user() if user: account = OpenLibraryAccount.get_by_email(user.email) ia_itemname = account.itemname if account else None s3_keys = web.ctx.site.store.get(account._key).get('s3_keys') if not user or not ia_itemname or not s3_keys: web.setcookie(config.login_cookie_name, "", expires=-1) redirect_url = "/account/login?redirect=%s/borrow?action=%s" % ( edition.url(), action) if i._autoReadAloud is not None: redirect_url += '&_autoReadAloud=' + i._autoReadAloud raise web.seeother(redirect_url) if action == 'return': lending.s3_loan_api(edition.ocaid, s3_keys, action='return_loan') stats.increment('ol.loans.return') raise web.seeother(edition.url()) elif action == 'join-waitinglist': lending.s3_loan_api(edition.ocaid, s3_keys, action='join_waitlist') stats.increment('ol.loans.joinWaitlist') raise web.redirect(edition.url()) elif action == 'leave-waitinglist': lending.s3_loan_api(edition.ocaid, s3_keys, action='leave_waitlist') stats.increment('ol.loans.leaveWaitlist') raise web.redirect(edition.url()) # Intercept a 'borrow' action if the user has already # borrowed the book and convert to a 'read' action. # Added so that direct bookreader links being routed through # here can use a single action of 'borrow', regardless of # whether the book has been checked out or not. elif user.has_borrowed(edition): action = 'read' elif action in ('borrow', 'browse'): borrow_access = user_can_borrow_edition(user, edition) if not (s3_keys or borrow_access): raise web.seeother(error_redirect) lending.s3_loan_api(edition.ocaid, s3_keys, action='%s_book' % borrow_access) stats.increment('ol.loans.bookreader') stats.increment('ol.loans.%s' % borrow_access) action = 'read' if action == 'read': bookPath = '/stream/' + edition.ocaid if i._autoReadAloud is not None: bookPath += '?_autoReadAloud=show' # Look for loans for this book user.update_loan_status() loans = get_loans(user) for loan in loans: if loan['book'] == edition.key: raise web.seeother(make_bookreader_auth_link( loan['_key'], edition.ocaid, bookPath, ol_host, ia_userid=ia_itemname )) # Action not recognized raise web.seeother(error_redirect)