def _confirm(req, info): import logging import manage_kbasix import manage_users import aux logging.debug('Starting confirmation') info['title'] = aux._fill_str(info['confirmation_title'], info) info['status_button_1'] = '' info['status_button_2'] = '' if 'token' in req.form: token = req.form['token'] else: token = '' session = manage_kbasix._check_token(token, first_time=True) if not session: info['class'] = 'fail' info[ 'details'] = 'Failed to confirm account due to an invalid or stale token (probably the account confirmation deadline has passed)' logging.warn(info['details'] + ' [%s]' % token) return aux._fill_page(info['status_page_'], info) else: session['login_name'] = manage_users._info( session['uid'])['login_name'] info.update(session) manage_users._mod(info['login_name'], {'locked': False}) manage_kbasix._delete_token(info['uid']) profile = manage_kbasix._account_info(info['login_name'], 'profile') info.update(profile) info['class'] = 'success' info['details'] = aux._fill_str(info['successful_confirmation_blurb'], info) logging.info('Successfully confirmed account "%s"' % info['login_name']) return aux._fill_page(info['status_page_'], info)
def _send_token(req, info): """Send an email token if the password was forgotten. _send_token(req, info) Returns a status page. """ import logging import os import manage_kbasix import manage_users import smtplib import aux from email.mime.text import MIMEText logging.debug('Starting forgotten password procedure for "%s"' % \ info['login_name']) if not manage_users._info(info['login_name']) or \ manage_users._info(info['login_name'])['locked'] or \ info['login_name'] in info['banned_logins']: info['class'] = 'fail' info['main_header'] = aux._make_header(info) info['title'] = 'Password reset' info['details'] = 'Either that user name was not found, you are \ not allowed into the system, or the account has yet to be activated.' info['status_button_1'] = '' info['status_button_2'] = '' logging.debug('Token not sent to "%s" because "%s"' % \ (info['login_name'], info['details'])) return aux._fill_page(info['status_page_'], info) uid = manage_users._info(info['login_name'])['uid'] user_email = manage_kbasix._account_info(info['login_name'], \ 'profile')['user_email'] # The token will only allow access to the profile module (which allows # to change the password). It should work from any IP address. token = manage_kbasix._create_token(req, uid, ip_set=False, \ access='profile.py') www_path = os.path.dirname((req.subprocess_env['SCRIPT_NAME'])) info['profile_url'] = \ req.construct_url(www_path + \ '/profile.py/process?start&token=%s' % token) msg = MIMEText(aux._fill_str(info['reset_password_notice'], info)) msg['Subject'] = aux._fill_str(info['reset_password_subject'], info) msg['From'] = info['reset_password_from_email_'] msg['To'] = user_email s = smtplib.SMTP(info['smtp_host_']) s.sendmail(info['reset_password_from_email_'], [user_email], \ msg.as_string()) s.quit() info['class'] = 'success' info['main_header'] = aux._make_header(info) info['title'] = 'Password reset' info['details'] = aux._fill_str(info['reset_password_blurb'], info) info['status_button_1'] = '' info['status_button_2'] = '' logging.info('Token sent due to forgotten password by "%s" to "%s"' % \ (info['login_name'], user_email)) return aux._fill_page(info['status_page_'], info)
def _check_quota(req, up_type, info): """Check the user's quota. 'up_type' is a string such as 'upload' or 'copy', depending on how the file is being added. Only '-file' files are counted towards quota. (user_dir_size, status) = _check_quota(req, up_type, info) Returns an (int, str) tuple with the user's space usage and an empty string if underquota, the number -1 and a "you are over quota" page (as a string) if overquota. Note that the quota doesn't impede a user from going over it, it just disallows adding files after it has been exceeded. Worst case scenario uses up approximately 'quota + max upload size'. """ import aux import logging user_dir_size = aux._get_dir_size(info) logging.debug('Space usage: %s/%s (%s)' % \ (user_dir_size, info['quota'], info['login_name'])) if user_dir_size >= info['quota']: info['user_dir_size'] = user_dir_size info['details'] = aux._fill_str(info['quota_limit_blurb'], info) info['class'] = 'fail' info['status_button_1'] = aux._go_back_button(req, info['token']) info['status_button_2'] = '' logging.info('File %s forbidden due to quota limits (%s)' % \ (up_type, info['login_name'])) info['title'] = up_type.capitalize() return (-1, aux._fill_page(info['status_page_'], info)) else: return (user_dir_size, '')
def _logout(token, info): """Logout a user. _logout(token, info) Returns the logout status page. """ import logging import manage_kbasix import aux uid = int(token.split('-')[0]) manage_kbasix._delete_token(uid, token='*') info['token'] = '' info['title'] = aux._fill_str(info['goodbye_title'], info) info['class'] = 'information' info['details'] = aux._fill_str(info['goodbye_blurb'], info) info['status_button_1'] = '' info['status_button_2'] = '' info['main_header'] = aux._make_header(info) logging.debug('Successful logout (%s)' % info['login_name']) return aux._fill_page(info['status_page_'], info)
def _confirmation_email(req, info): """Send a confirmation email to complete registration. _confirmation_email(req, info) Returns nothing. """ import logging import os import manage_kbasix import manage_users import smtplib import aux from email.mime.text import MIMEText logging.debug('Sending confirmation email for "%s"' % \ info['login_name']) uid = manage_users._info(info['login_name'])['uid'] # We set 'ip_set=False' so that users can confirm their account # from any IP. token = manage_kbasix._create_token(req, uid, ip_set=False) www_path = os.path.dirname((req.subprocess_env['SCRIPT_NAME'])) confirm_urn = '/confirm.py/process?start&token=%s' % token info['confirm_url'] = req.construct_url(www_path + confirm_urn) msg = MIMEText(aux._fill_str(info['confirmation_notice'], info)) msg['Subject'] = aux._fill_str(info['email_subject'], info) msg['From'] = info['email_from_'] msg['To'] = info['user_email'] try: s = smtplib.SMTP(info['smtp_host_']) s.sendmail(info['email_from_'], [info['user_email']], \ msg.as_string()) s.quit() except Exception as reason: logging.error('Unable to send confirmation email because \ "%s" (%s)' % (reason, info['login_name'])) raise ConfirmationEmailError(reason) logging.debug('Confirmation email sent sucessfully for "%s" to "%s"' % \ (info['login_name'], info['user_email'])) return
def _register(req, info): """Perform the registration process. _register(req, info) Returns the registration status page. """ import logging import re import time import manage_users import manage_kbasix import aux # User names will be unique, case-insensitive and case-aware. info['user_name'] = req.form['user_name'].value info['login_name'] = info['user_name'].lower() logging.info('Registering new account for "%s"' % info['login_name']) if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: allow_internal = True else: allow_internal = False info['user_email'] = req.form['user_email'].value err = 0 info['details'] = '' # KBasix is not yet tested on different locales, so we enforce # ASCII here. try: info['login_name'].encode('ascii') info['user_email'].encode('ascii') except: err += 1 info['details'] += 'Your user name and email address must be \ ASCII.<br>' if info['login_name'] in info['reserved_login_names']: err += 1 # This might be a lie, but the user need not know that. info['details'] += 'That user name is already taken.<br>' if info['login_name'] in info['blacklisted_login_names']: err += 1 info['details'] += 'That user name is not permitted.<br>' # Keep in mind that, regardless of %(login_name_max_length)s, problems # may arise if the file paths are too long (this is an issue with # with the fact that the user name is present as part of the "world" # URN). Note that there is a 100 character hard limit, regardless # of 'login_name_max_length'. if len(info['login_name']) > info['login_name_max_length']: err += 1 info['details'] += 'That user name is too long.<br>' if not info['login_name'].isalnum(): err += 1 info['details'] += 'Your user name can only contain letters and/or \ numbers.<br>' # To avoid UID look-alike names we can set 'alpha_start_login_name' if info['alpha_start_login_name'] and len(info['login_name']) > 0 and \ not info['login_name'][0].isalpha(): err += 1 info['details'] += 'The user name must begin with a letter.<br>' if len(info['user_email']) > info['email_max_length']: err += 1 info['details'] += 'That email address is too long.<br>' if not re.match(info['valid_email_'], info['user_email'], re.I): err += 1 info['details'] += 'Please input a valid email address.<br>' if 'use_ldap' in req.form: info['user_password'] = '******' info['auth_method'] = 'ldap' info['ldap_server'] = req.form['ldap_server'].value if info['ldap_server']: info['user_ldap_name'] = req.form['user_ldap_name'].value info['user_ldap_password'] = \ req.form['user_ldap_password'].value (err, info['details']) = _check_ldap(info, err) else: err += 1 info['details'] += 'Please specify the LDAP server.<br>' elif allow_internal: info['user_password'] = req.form['user_password'].value info['user_password_check'] = req.form['user_password_check'].value info['auth_method'] = '' info['ldap_server'] = info['user_ldap_name'] = '' (err, info['details']) = _check_password(info, err) else: err += 1 info['details'] += 'Unavailable authentication scheme.<br>' if not err: deadline = time.time() + info['account_confirmation_timeout'] try: (OK, dt) = manage_users._user_add(login_name = \ info['login_name'], \ password = \ info['user_password'], \ auth_method = \ info['auth_method'], \ user_auth_name = \ info['user_ldap_name'], \ auth_server = \ info['ldap_server'], \ expires = deadline) if OK: now = time.time() profile = {'registered': now, \ 'login_name': info['login_name'], \ 'user_name': info['user_name'], \ 'user_email': info['user_email'], \ 'quota': info['default_quota'], \ 'last_login': now} manage_kbasix._account_add(info['login_name'], profile) except Exception as reason: raise RegisterError(reason) if not OK: err += 1 info['details'] += dt else: try: _confirmation_email(req, info) except: err += 1 info['details'] += 'Unable to send confirmation email.<br>' if err: info['class'] = 'fail' info['status_button_1'] = aux._go_back_button(req, info['token']) info['status_button_2'] = '' logging.info('Failed account registration for "%s" because "%s"' % \ (info['login_name'], \ info['details'].replace('<br>',' '))) else: info['class'] = 'success' info['details'] += \ aux._fill_str(info['successful_registration_blurb'], info) info['status_button_1'] = '' info['status_button_2'] = '' logging.info('Successful account registration for "%s"' % \ info['login_name']) info['title'] = 'Registration' return aux._fill_page(info['status_page_'], info)
def _get_file_list(info): """Get the file listing. file_list = _get_file_list(info) Returns a string. """ # This function is way too big, it needs to be sensibly split up. import logging import os import fnmatch import time import aux import manage_users import manage_kbasix login_name = info['login_name'] logging.debug('Creating a file list (%s)' % login_name) prefs = manage_kbasix._account_info(login_name, 'prefs') user_dir = os.path.join(info['users_root_dir_'], str(info['uid'])) entries = {} count = {} gids = manage_users._info(login_name)['gids'] # Check for local- and GID-shared files. for subdir in ['local'] + [str(i) for i in gids]: subpath = os.path.join(info['shared_dir_'], subdir) if os.path.exists(subpath): # Some house cleaning: delete broken symlinks. for i in os.listdir(subpath): f = os.path.join(subpath, i) # We are lenient with file removals in shared directories # because another user might have beat us to it. if not os.path.exists(f): try: os.remove(f) logging.info('Deleted broken shared symlink "%s" \ (%s)' % (f, login_name)) except Exception as reason: logging.warn(reason) for i in fnmatch.filter(os.listdir(subpath), '*-file'): rel_path = os.path.relpath(subpath, user_dir) src = os.path.join(rel_path, i) dst = os.path.join(user_dir, i) # Shared entries which are deleted by the user (the # sharee) are actually hidden from view by adding them # to the 'hidden_gidloc_shared_files' list when the # sharee deletes them. # Once hidden, local/gid-shared files cannot be recovered # unless explicitly re-shared with that user via uid_shares. # Note that if the file exists (important if it's a uid # point share, which takes precedence) we leave it alone. if not os.path.exists(dst) and \ i not in \ prefs['file_manager']['hidden_gidloc_shared_files']: try: # The target may not exist, but if the link does, # delete it. if os.path.islink(dst + '-id'): os.remove(dst + '-id') os.symlink(src + '-id', dst + '-id') except: logging.error('Cannot link shared id file "%s" \ (%s)' % (src + '-id', login_name)) else: if os.path.islink(dst): os.remove(dst) os.symlink(src, dst) logging.debug('Added shared file "%s" -> "%s" \ (%s)' % (src, dst, login_name)) # Create the listing of the files/symlinks in the user's directory. for i in os.listdir(user_dir): f = os.path.join(user_dir, i) if not os.path.exists(f): try: os.remove(f) logging.info('Deleted broken user symlink "%s" (%s)' % \ (f, login_name)) except Exception as reason: logging.error(reason) for i in fnmatch.filter(os.listdir(user_dir), '*-id'): f = os.path.join(user_dir, i) # This should not normally happen. The magic [:-3] deletes '-id' # and leaves the content file name. if not os.path.exists(f[:-3]): try: os.remove(f) logging.error('Deleted orphan id file "%s" (%s)' % \ (f, login_name)) except Exception as reason: logging.error(reason) continue details = manage_users._read_file(f, lock=False) # A symlink implies the file is being shared with the user. if os.path.islink(f): details['shared_with_me'] = True else: details['shared_with_me'] = False for j in ['file_title', 'file_description']: if not details[j]: details[j] = info['empty_placeholder_'] details['file_date'] = \ time.strftime(info['file_manager_time_format_'], \ time.localtime(details['timestamp'])) # We need the token for the buttons. details['token'] = info['token'] sort_key = details[prefs['file_manager']['sort_criteria']] # We pad with zeros to obtain a natural sorting for entries with # the same non-string keys e.g. size or timestamp. Padding to 50 # seems safe, although this is admittedly a magic number # (timestamps have less than 20 digits, and files size are smaller # than that). Worst case scenario is that the sorting on huge # numeric sort keys (larger than 50 digits) will be wrong, but this # is unlikely to happen and has no other consequences. if not isinstance(sort_key, basestring): sort_key = '%r' % sort_key sort_key = sort_key.zfill(50) # We internally distinguish between identical entry values # (say, identical file names), but although lumped together # in their proper place within the overall list they are # not sub-sorted in any deliberate way. if sort_key not in entries: count[sort_key] = 1 entries[sort_key] = details else: count[sort_key] += 1 entries[sort_key + ' (%s)' % count[sort_key]] = details if prefs['file_manager']['condensed_view']: info['template'] = 'condensed_entry_template_' else: info['template'] = 'entry_template_' file_list = '' for key in sorted(entries, \ reverse=bool(prefs['file_manager']['reverse'])): entries[key]['shared_status'] = '' entries[key]['file_colour'] = info['my_file_colour_'] if entries[key]['shared_with_me']: # World shares are not symlinked with the individual accounts, # and entries which cannot be read any longer are deleted. if not entries[key]['local_share'] and \ not info['uid'] in entries[key]['uid_shares'] and not \ set(gids).intersection(set(entries[key]['gid_shares'])): shared_file = \ os.path.join(user_dir, entries[key]['file_tag']) try: os.remove(shared_file) os.remove(shared_file + '-id') except Exception as reason: logging.error(reason) continue # Don't show the shares made by no-longer-exsting users if # 'exusers_cannot_share' is True. elif not manage_users._info(entries[key]['owner_uid']) and \ info['exusers_cannot_share']: continue else: entries[key]['shared_status'] = """ <img src="%s" title="File shared with me by: %s" alt="[File shared with me by: %s]" /> """ % (info['shared_icon_with_me_'], entries[key]['owner'], \ entries[key]['owner']) entries[key]['file_colour'] = info['other_file_colour_'] if prefs['file_manager']['hide_shared'] and \ entries[key]['shared_with_me']: continue # Shared files can be copied internally (it's more efficient than # downloading and uploading again). entries[key]['copy_file'] = '' if entries[key]['shared_with_me']: entries[key]['copy_file_icon_'] = info['copy_file_icon_'] entries[key]['copy_file_form_style_'] = \ info['copy_file_form_style_'] entries[key]['copy_file'] = """ <form %(copy_file_form_style_)s action="../file_manager.py/process?action=copy_file" method="post"> <input type="hidden" name="token" value="%(token)s" /> <input type="hidden" name="file_tag" value="%(file_tag)s" /> <input title="Make a local copy" type="image" alt="Make a local copy" src="%(copy_file_icon_)s" /> </form> """ % entries[key] # Files that are shared (the user being the sharer) are tagged # in various ways to indicate this. else: if entries[key]['world_share']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with the world" alt="[File is shared with the world]" /> """ % info['shared_icon_by_me_to_world_'] if entries[key]['local_share']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with registered users" alt="[File is shared with registered users]" /> """ % info['shared_icon_by_me_locally_'] if entries[key]['gid_shares']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with selected groups" alt="[File is shared with selected groups]" /> """ % info['shared_icon_by_me_to_groups_'] if entries[key]['uid_shares']: entries[key]['shared_status'] += """ <img src="%s" title="File is shared with selected users" alt="[File is shared with selected users]" /> """ % info['shared_icon_by_me_selectively_'] # Convert the size to a string with the appropriate unit suffix e.g. # 'MB'. entries[key]['file_size_str'] = \ aux._bytes_string(entries[key]['file_size']) entries[key]['toggle_select'] = info['toggle_select'] # Limit the length of titles and descriptions. for i in ['description', 'title']: if info['max_chars_in_' + i]: max_char = len(entries[key]['file_' + i]) char_num = min(max_char, info['max_chars_in_' + i]) entries[key][i + '_blurb'] = \ entries[key]['file_' + i][:char_num] if max_char > char_num: entries[key][i + '_blurb'] += '...' else: entries[key][i + '_blurb'] = entries[key]['file_' + i] # The keyword filter. This is actually a very important # functionality, and should be split into its own function. # Furthermore, it is currently very weak, and should be drastically # improved. Right now is just filters on words in the file title # and description, ignoring punctuation. It is also # case-insensitive. if prefs['file_manager']['keywords']: import re # Use '[\W_]+' to eliminate underscores. See also: # # http://stackoverflow.com/questions/6631870/strip-non-alpha-numeric-characters-from-string-in-python-but-keeping-special-cha # # The 're.U' works on unicode strings. nonalnum = re.compile('[\W]+', re.U) keywords = set([i.lower() for i in \ prefs['file_manager']['keywords'].split()]) words = [nonalnum.sub('', i.lower()) for i in \ entries[key]['file_title'].split()] words += [nonalnum.sub('', i.lower()) for i in \ entries[key]['file_description'].split()] if keywords <= set(words): file_list += aux._fill_str(info[info['template']], \ entries[key]) continue file_list += aux._fill_str(info[info['template']], entries[key]) if not file_list: return info['no_files_found_'] else: return file_list
def _get_file(req, info): """Upload a file (-file) and create its associate metadata (-file-id) file. _get_file(req, info) Returns the KBasix upload status page. """ import os import cgi import time import hashlib import json import aux import logging from mod_python import apache # Definitions. # A time/hash pair is something like this: # # time 1345131658.602529 # hash 60f6bee3404d57f04bb47d9369da5b20e4e510eb0ced4011f3de065590407399 # # id and hash are the same string # '-file' and '-file-id' are those strings, so that time-hash-file-id # is something like: # # 1345131658.602529-60f6bee3404d...407399-file-id # # id_tag is time-hash # file_tag is time-hash-file # (this file_tag is also the name of the actual data file) # id_file is time-hash-file-id # (this id_file is also the name of the actual metadata file) id_tag = '%r-%s' % \ (time.time(), \ hashlib.sha256(os.urandom(info['random_length'])).hexdigest()) file_tag = id_tag + '-file' # The user directories are determined by their UID. They are created # when the account is created. Remember that UID is always an integer. file_out = os.path.join(info['users_root_dir_'], str(info['uid']), \ file_tag) id_file = file_out + '-id' fileitem = req.form['file_name'] # In order to substitute this value into the message strings it must # be decoded. file_in = fileitem.filename.decode('utf-8') if file_in: d = hashlib.md5() # Buffer size is set to 2^16, a magic number which seems OK, # but should be justified (or changed). Upping to 2**24 didn't # seem to make a difference. Note this also has to be changed # in the _fbuffer function above. # Browsers seem to be able to handle files up to 2GB in size. # Uploading a 2082168350b binary file on a 100Mb LAN took about # 4 minutes. # Progress bars are a no-go (for now?): # http://www.mailinglistarchive.com/[email protected]/msg01880.html logging.info('Starting to upload "%s" (%s)' % \ (file_out, info['login_name'])) try: f = open(file_out, 'wb', 2**16) for chunk in _fbuffer(fileitem.file): d.update(chunk) f.write(chunk) except Exception as reason: logging.error('Unable to upload the file type because \ "%s" (%s)' % (reason, info['login_name'])) raise GetFileError('Upload failed, closing "%s"' % file_out) finally: f.close() os.chmod(file_out, 0600) s = os.path.getsize(file_out) # You can add to the "id_info" dictionary any other metadata # you wish to save, but the following is the basic information # all files will contain. id_info = {} id_info['owner'] = info['login_name'] id_info['owner_uid'] = info['uid'] id_info['uid_shares'] = [] id_info['gid_shares'] = [] id_info['local_share'] = False id_info['world_share'] = False id_info['timestamp'] = time.time() id_info['file_title'] = req.form['file_title'].value id_info['file_description'] = req.form['file_description'].value file_type = req.form['file_type'].value if file_type not in info['file_types']: id_info['file_type'] = 'Unknown' else: id_info['file_type'] = file_type id_info['file_name'] = file_in id_info['file_md5sum'] = d.hexdigest() id_info['file_size'] = s id_info['file_tag'] = file_tag # Documentation for python-magic seems non-existent, but it seems # to be equivalent to the "file" command (which isn't that great). try: import magic magic_type = magic.open(magic.MAGIC_NONE) magic_type.load() id_info['magic_type'] = magic_type.file(file_out) except Exception as reason: logging.error('Unable to determine the file type because \ "%s" (%s)' % (reason, info['login_name'])) id_info['magic_type'] = 'Unknown' id_info['custom'] = {} for key in id_info: if isinstance(id_info[key], basestring): # We should use html.escape when migrating to python3 id_info[key] = cgi.escape(id_info[key], True) try: f = open(id_file, 'wb') json.dump(id_info, f) except Exception as reason: logging.error('Unable to create the metadata file because \ "%s" (%s)' % (reason, info['login_name'])) if os.isfile(file_out): os.remove(file_out) raise GetFileError('Metadata creation failed, deleted upload \ file "%s" and closing "%s"' % (file_out, id_file)) finally: f.close() os.chmod(id_file, 0600) n = id_info['file_name'] if s == 0: info['class'] = 'warning' info['details'] = 'File "%s" uploaded but empty (it may be \ client-side unreadable)' % n else: s = aux._bytes_string(s) info['class'] = 'success' info['details'] = aux._fill_str(info['successful_upload_'], \ {'name': \ id_info['file_name'], \ 'type': \ id_info['file_type'], \ 'size': s, \ 'md5': d.hexdigest()}) logging.info('Finished uploading and saving "%s" (%s)' % \ (file_out, info['login_name'])) else: info['class'] = 'fail' info['details'] = 'No file was specified.' info['title'] = 'File Upload' # We don't use the usual "go back" button because it will show the # upload animation. info['status_button_1'] = """ <form action="../upload.py/process?start" method="post"> <input type="hidden" name="token" value="%(token)s" /> <input type="submit" value="Back" /> </form> """ % info info['status_button_2'] = '' return aux._fill_page(info['status_page_'], info)
def _initialize(req, info): """Initialize the metadata editor page. _initialize(req, info) Returns the KBasix metadata editor page. """ import logging import aux import file_manager file_info = file_manager._get_file_info(info['file_tag'], info) # If 'file_info' is empty then that file has been unshared from # underneath the user, i.e. they couldn't have edited either way. if not file_info or file_info['owner_uid'] != info['uid']: info['title'] = 'Metadata unavailable' info['details'] = \ 'You cannot edit the metadata of a file you do not own.' info['status_button_1'] = """ <form action="../file_manager.py/process?start" method="post"> <input type="hidden" name="token" value="%s" /> <input type="submit" value="Back" /> </form> """ % info['token'] info['status_button_2'] = '' info['class'] = 'warning' return aux._fill_page(info['status_page_'], info) else: logging.debug('Starting the metadata editor (%s)' % \ info['login_name']) info['file_manager_button'] = """ <form action="../file_manager.py/process?start" method="post"> <input type="hidden" name="token" value="%s" /> <input type="submit" value="Files" /> </form> """ % info['token'] entry = {} # We need to translate the numeric UIDs and GIDs in actual # names. Also, the local and world booleans are converted # into their HTML equivalents. (entry['uid_shares'], entry['gid_shares'], entry['local_share'], \ entry['world_share']) = _get_shares(file_info, info) # If a file has been shared with the world, i.e. if it's directly # available via http the URL is set here. if entry['world_share']: import manage_kbasix import os user_name = manage_kbasix._account_info(info['login_name'], \ 'profile')['user_name'] entry['world_url'] = \ os.path.join(info['www_url_'], user_name, info['file_tag']) else: entry['world_url'] = '' info['boolean_data'] = aux._fill_str(info['boolean_meta_'], entry) # There are four types of metadata: # core metadata: set upon creation e.g. MD5SUM. # basic metadata: non-boolean, user-editable subset of the core # metadata e.g. UID shares. # boolean metadata: two booleans which toggles between sharing # with registered users/world. # custom metadata: metadata created by the user. # Note that basic is just an arbitrary distinction, whilst custom is # stored under a different key (file_info['custom']). Basic metadata # is defined by metaeditor['basic_meta'] in defs.py. # # The 'meta_template_' is the template defined in defs.py which # controls the information and layout of the entry. info['basic_data'] = '' for key, value in file_info.items(): if key not in info['basic_meta']: continue data = {} data['key'] = key # The shares are replaced by the names, not the numeric values. if key in ['uid_shares', 'gid_shares']: data['value'] = entry[key] else: data['value'] = value data.update(info['basic_meta'][key]) info['basic_data'] += aux._fill_str(info['meta_template_'], data) info['custom_data'] = '' for key, value in file_info['custom'].items(): data = {} data['key'] = key data['name'] = key data['value'] = value data['help'] = '' info['custom_data'] += aux._fill_str(info['meta_template_'], data) info['file_name'] = file_info['file_name'] return aux._fill_page(info['metaeditor_page_'], info)
def _login(req, info): """Try to login the user, or send an email token if the password is forgotten. _login(req, info) Returns a status page. """ if 'forgot_password' in req.form: try: return _send_token(req, info) except Exception as reason: raise SendTokenError(reason) import logging from mod_python import apache import manage_users import manage_kbasix import time import aux from mod_python import util logging.debug('Starting login process for "%s"' % info['login_name']) if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: allow_internal = True else: allow_internal = False password = req.form['user_password'].value # Note that '_authenticate' only returns the uid if 'is_usr' is # True, otherwise it'll return a keyword specifying the authentication # failure point. try: (is_usr, status) = manage_users._authenticate(info['login_name'], \ password) logging.info('Authentication for "%s" was "%s" with \ status/uid: %s' % (info['login_name'], is_usr, status)) except Exception as reason: raise LoginError(reason) blocked = False # We don't just show the reasons for authentication failures, but # instead use codes defined in 'defs.py' (the log is explicit in this # respect). if is_usr: uid = status locked = manage_users._info(info['login_name'])['locked'] if locked: blocked = True msg = info['reason_not_active_'] else: blocked = True msg = info['reason_auth_fail_'] if not blocked: auth_method = manage_users._info(info['login_name'])['auth_method'] # An empty 'auth_method' means the auth is internal. if not auth_method and not allow_internal: blocked = True msg = info['reason_not_allowed_'] elif info['login_name'] in info['banned_logins']: blocked = True msg = info['reason_banned_'] if not blocked: try: info['token'] = manage_kbasix._create_token(req, uid) info['user_name'] = \ manage_kbasix._account_info(info['login_name'], \ 'profile')['user_name'] info['access'] = 'all' info['class'] = 'information' info['main_header'] = aux._make_header(info) info['title'] = aux._fill_str(info['welcome_title'], info) info['details'] = aux._fill_str(info['welcome_blurb'], info) info['status_button_1'] = """ <form action="../%(referrer)s/process?start" method="post"> <input type="hidden" name="token" value="%(token)s"> <input type="submit" value="Continue" /> </form> """ % info info['status_button_2'] = '' manage_kbasix._account_mod(info['login_name'], \ 'profile', {'last_login': \ time.time()}) logging.info('Successful login from %s (%s)' % \ (req.get_remote_host(apache.REMOTE_NOLOOKUP), \ info['login_name'])) return aux._fill_page(info['status_page_'], info) except Exception as reason: raise LoginError(reason) else: info['class'] = 'fail' info['title'] = 'Login' info['details'] = aux._fill_str(msg, info) info['status_button_1'] = aux._go_back_button(req, token = '') info['status_button_2'] = '' logging.info('Failed login for "%s" because "%s"' % \ (info['login_name'], info['details'])) return aux._fill_page(info['status_page_'], info)
def _update(req, info): import logging import re import time import manage_users import manage_kbasix import aux if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: allow_internal = True else: allow_internal = False profile = {} account = {} account['first_name'] = req.form['first_name'].value account['last_name'] = req.form['last_name'].value profile['user_email'] = req.form['user_email'].value err = 0 info['details'] = '' if not re.match(info['valid_email_'], profile['user_email'], re.I): err += 1 info['details'] += 'Please input a valid email address.<br>' if 'user_password' in req.form: info['user_password'] = req.form['user_password'].value else: info['user_password'] = '' info['user_ldap_password'] = req.form['user_ldap_password'].value info['user_ldap_name'] = req.form['user_ldap_name'].value was_external = manage_users._info(info['login_name'])['auth_method'] if 'use_ldap' in req.form and info['user_ldap_password']: if not info['user_ldap_name']: err += 1 info['details'] += 'Please fill out all the LDAP credentials.<br>' else: from register import _check_ldap info['ldap_server'] = req.form['ldap_server'].value if info['ldap_server']: (err, info['details']) = _check_ldap(info, err) else: err += 1 info['details'] += 'Please specify the LDAP server.<br>' if not err: account['password'] = '******' account['auth_method'] = 'ldap' account['auth_server'] = info['ldap_server'] account['user_auth_name'] = info['user_ldap_name'] if not was_external: info[ 'details'] += 'Authentication is no longer internal.<br>' elif info['user_password'] and allow_internal: from register import _check_password info['user_password_check'] = req.form['user_password_check'].value (err, info['details']) = _check_password(info, err) if not err: account['auth_method'] = '' account['auth_server'] = account['user_auth_name'] = '' account['password'] = info['user_password'] if was_external: info['details'] += 'Authentication is now internal.<br>' else: info['details'] += 'Note: no authentication changes made.<br>' if not err: try: manage_kbasix._account_mod(info['login_name'], 'profile', profile) manage_users._mod(info['login_name'], account) except Exception as reason: raise UpdateError(reason) if err: info['class'] = 'fail' info['status_button_1'] = aux._go_back_button(req, info['token']) info['status_button_2'] = '' logging.debug('Failed profile update because "%s" (%s)' % \ (info['details'].replace('<br>',' '), info['login_name'])) else: info['class'] = 'success' info['details'] += aux._fill_str(info['successful_update_blurb'], info) if info['access'] == 'profile.py': info['status_button_1'] = """ <form action="../login.py/process?start" method="post"> <input type="submit" value="Login" /> </form><br> """ % info else: info['status_button_1'] = '' info['status_button_2'] = '' logging.debug('Successful profile update (%s)' % info['login_name']) info['title'] = 'User Profile' return aux._fill_page(info['status_page_'], info)
def _update(req, info): import logging import re import time import manage_users import manage_kbasix import aux if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: allow_internal = True else: allow_internal = False profile = {} account = {} account['first_name'] = req.form['first_name'].value account['last_name'] = req.form['last_name'].value profile['user_email'] = req.form['user_email'].value err = 0 info['details'] = '' if not re.match(info['valid_email_'], profile['user_email'], re.I): err += 1 info['details'] += 'Please input a valid email address.<br>' if 'user_password' in req.form: info['user_password'] = req.form['user_password'].value else: info['user_password'] = '' info['user_ldap_password'] = req.form['user_ldap_password'].value info['user_ldap_name'] = req.form['user_ldap_name'].value was_external = manage_users._info(info['login_name'])['auth_method'] if 'use_ldap' in req.form and info['user_ldap_password']: if not info['user_ldap_name']: err += 1 info['details'] += 'Please fill out all the LDAP credentials.<br>' else: from register import _check_ldap info['ldap_server'] = req.form['ldap_server'].value if info['ldap_server']: (err, info['details']) = _check_ldap(info, err) else: err += 1 info['details'] += 'Please specify the LDAP server.<br>' if not err: account['password'] = '******' account['auth_method'] = 'ldap' account['auth_server'] = info['ldap_server'] account['user_auth_name'] = info['user_ldap_name'] if not was_external: info['details'] += 'Authentication is no longer internal.<br>' elif info['user_password'] and allow_internal: from register import _check_password info['user_password_check'] = req.form['user_password_check'].value (err, info['details']) = _check_password(info, err) if not err: account['auth_method'] = '' account['auth_server'] = account['user_auth_name'] = '' account['password'] = info['user_password'] if was_external: info['details'] += 'Authentication is now internal.<br>' else: info['details'] += 'Note: no authentication changes made.<br>' if not err: try: manage_kbasix._account_mod(info['login_name'], 'profile', profile) manage_users._mod(info['login_name'], account) except Exception as reason: raise UpdateError(reason) if err: info['class'] = 'fail' info['status_button_1'] = aux._go_back_button(req, info['token']) info['status_button_2'] = '' logging.debug('Failed profile update because "%s" (%s)' % \ (info['details'].replace('<br>',' '), info['login_name'])) else: info['class'] = 'success' info['details'] += aux._fill_str(info['successful_update_blurb'], info) if info['access'] == 'profile.py': info['status_button_1'] = """ <form action="../login.py/process?start" method="post"> <input type="submit" value="Login" /> </form><br> """ % info else: info['status_button_1'] = '' info['status_button_2'] = '' logging.debug('Successful profile update (%s)' % info['login_name']) info['title'] = 'User Profile' return aux._fill_page(info['status_page_'], info)
def _update(req, info): """Update the metadata. _update(req, info) Returns a status web page. """ import logging import aux import cgi import file_manager info['file_tag'] = req.form['file_tag'].value logging.debug('Updating metadata of "%s" (%s)' % \ (info['file_tag'], info['login_name'])) info['title'] = 'Metadata Editor' # This is a generic status button, we make the variable substitutions # below. info['status_button_1'] = """ <form action="../metaeditor.py/process?start&file_tag=%(file_tag)s" method="post"> <input type="hidden" name="token" value="%(token)s" /> <input type="submit" value="Back" /> </form> """ info['status_button_2'] = '' file_manager._check_file_tag(info['file_tag'], info['login_name']) file_info = file_manager._get_file_info(info['file_tag'], info) if not file_info or file_info['owner_uid'] != info['uid']: raise UpdateError('Invalid file ownership "%s" (%s)' % \ (info['file_tag'], info['login_name'])) info['details'] = '' form_dict = req.form if 'local_share' not in form_dict: form_dict['local_share'] = '' if 'world_share' not in form_dict: form_dict['world_share'] = '' # Single quotes are escaped (string is inside '') # Without .value: 'file_type': Field('file_type', 'Unknown') # With .value: 'file_type': 'Unknown' for key, rawval in form_dict.items(): val = cgi.escape(rawval.value, True) if key in info['basic_meta'] or key in \ ['local_share', 'world_share']: if key in ['uid_shares', 'gid_shares', 'local_share', \ 'world_share']: err = '' try: val.encode('ascii') except: info['class'] = 'fail' info['details'] += 'Shares must be ASCII-encoded' info['status_button_1'] = info['status_button_1'] % info logging.debug('Non-ASCII shares found (%s)' % \ info['login_name']) return aux._fill_page(info['status_page_'], info) logging.debug('Processing share "%s" with input value \ "%s" (%s)' % (key, val, info['login_name'])) # Point shares are done with specific users and groups. # Wide shares are with all registered users or the world. if key in ['uid_shares', 'gid_shares']: (file_info[key], err) = \ _set_point_shares(file_info[key], val, key, info) if key in ['local_share', 'world_share']: file_info[key] = \ _set_wide_share(file_info[key], val, key, info) info['details'] += err else: file_info[key] = val elif key in file_info['custom']: file_info[key] = val elif key in ['token', 'file_tag', 'action']: pass else: try: key.encode('ascii') except: key = 'non-ASCII key' try: val.encode('ascii') except: val = 'non-ASCII value' logging.warn('Ignoring unknown key pair "%s: %s" (%s)' % \ (key, val, info['login_name'])) _save_file_info(file_info, info['file_tag'], info) logging.debug('Successful metadata update (%s)' % info['login_name']) info['class'] = 'success' info['details'] += aux._fill_str(info['successful_update_blurb'], info) info['status_button_1'] = info['status_button_1'] % info return aux._fill_page(info['status_page_'], info)