def _initialize(info): import logging import manage_kbasix import manage_users import aux profile = manage_kbasix._account_info(info['login_name'], 'profile') if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: info['internal_disabled'] = '' else: info['internal_disabled'] = 'disabled' user = manage_users._info(info['login_name']) info['first_name'] = user['first_name'] info['last_name'] = user['last_name'] if user['auth_method'] == 'ldap': info['use_ldap'] = 'checked' else: info['use_ldap'] = '' info['ldap_options'] = '' for key in info['ldap_servers']: if user['auth_server'] == info['ldap_servers'][key]['server_']: info['ldap_servers'][key]['selected_'] = 'selected' info['ldap_options'] += info['ldap_servers'][key]['option_'] % info[ 'ldap_servers'][key] info['user_ldap_name'] = user['user_auth_name'] info.update(profile) logging.debug('Starting profile page (%s)' % info['login_name']) return aux._fill_page(info['profile_page_'], info)
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 _initialize(info): import logging import manage_kbasix import manage_users import aux profile = manage_kbasix._account_info(info['login_name'], 'profile') if '*' in info['allowed_internal_logins'] or \ info['login_name'] in info['allowed_internal_logins']: info['internal_disabled'] = '' else: info['internal_disabled'] = 'disabled' user = manage_users._info(info['login_name']) info['first_name'] = user['first_name'] info['last_name'] = user['last_name'] if user['auth_method'] == 'ldap': info['use_ldap'] = 'checked' else: info['use_ldap'] = '' info['ldap_options'] = '' for key in info['ldap_servers']: if user['auth_server'] == info['ldap_servers'][key]['server_']: info['ldap_servers'][key]['selected_'] = 'selected' info['ldap_options'] += info['ldap_servers'][key]['option_'] % info['ldap_servers'][key] info['user_ldap_name'] = user['user_auth_name'] info.update(profile) logging.debug('Starting profile page (%s)' % info['login_name']) return aux._fill_page(info['profile_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 process(req): """Process the welcome page. process(req) """ # We import functions "as needed" at this stage, making # sure they are private (underscore prefix). The exception # is "defs", which are just a bunch of defintions (although # any functions defined within it should be made private). from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, main # Here and in all other modules the module-specific definitions # take precedence over the 'kbasix' ones. info = {} info.update(kbasix) info.update(main) import logging logging.basicConfig(level=getattr(logging, \ info['log_level_'].upper()), \ filename=info['log_file_'], \ datefmt=info['log_dateformat_'], \ format=info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for main.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) try: session = _is_session(req, required=False) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: # We use 'warn' level because the '_is_session' redirect seems to # trigger this (if 'required=True') although it's not a critical # error. logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) try: return _initialize(info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize the main \ page [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info)
def process(req): """Process the logout page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, logout info = {} info.update(kbasix) info.update(logout) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for logout.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) try: session = _is_session(req, required=False) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) if not info['token']: info['title'] = 'Logout' info['class'] = 'information' info['details'] = 'You are not logged in.' info['status_button_1'] = '' info['status_button_2'] = '' return _fill_page(info['status_page_'], info) else: try: return _logout(info['token'], info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to terminate session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info)
def process(req): """Process the logout page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, logout info = {} info.update(kbasix) info.update(logout) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for logout.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) try: session = _is_session(req, required=False) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) if not info['token']: info['title'] = 'Logout' info['class'] = 'information' info['details'] = 'You are not logged in.' info['status_button_1'] = '' info['status_button_2'] = '' return _fill_page(info['status_page_'], info) else: try: return _logout(info['token'], info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to terminate session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info)
def process(req): """Process the registration page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, register info = {} info.update(kbasix) info.update(register) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for register.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot register over an insecure connection.' logging.info('Disallowed insecure access to register.py') return _fill_page(info['error_page_'], info) try: session = _is_session(req, required=False) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) if 'start' in req.form: try: return _initialize(info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize registration \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified trying to register.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'register': try: return _register(req, info) except RegisterError as reason: logging.critical(reason) info['details'] = '[SYS] Unable to register [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to register [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected action trying to register.' logging.error(info['details']) return _fill_page(info['error_page_'], info)
def process(req): """Process the file manager page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, file_manager info = {} info.update(kbasix) info.update(file_manager) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for file_manager.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot manage your file over an insecure \ connection.' logging.info('Disallowed insecure access to file_manager.py') return _fill_page(info['error_page_'], info) try: # The holdover is required if "per_request_token" is True, and # has no effect otherwise. This is needed due to the # client-generated download window (which has no concept of the # new request token). if 'action' in req.form and req.form['action'] == 'download': session = _is_session(req, required=True, holdover=True) else: session = _is_session(req, required=True) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) info['filter'] = False if 'start' in req.form: try: return _initialize(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize the file manager \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified within the file manager.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'filter': try: info['filter'] = True return _initialize(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] File manager error [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'copy_file': try: return _copy_file(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] File manager error [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'confirm_delete': try: return _confirm_delete(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] File manager error [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'delete': try: return _delete_file(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Error deleting file [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'confirm_bulk_delete': try: return _confirm_bulk_delete(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Error deleting files [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'download': try: return _download_file(req, info) except Exception as reason: # Not critical... user-end problem maybe? logging.error(reason) info['details'] = '[SYS] Unable to download file [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected file manager action.' logging.error(info['details']) return _fill_page(info['error_page_'], info)
def _delete_file(req, info): """Delete/hide a file and its respective metadata. _delete_file(req, info) Returns to the file manager page. """ import logging import os import manage_kbasix if 'file_tag' in req.form: file_tags = [req.form['file_tag'].value] elif 'file_tags' in req.form: file_tags_str = req.form['file_tags'].value file_tags = file_tags_str.split() for file_tag in file_tags: _check_file_tag(file_tag, info['login_name']) the_dir = os.path.join(info['users_root_dir_'], str(info['uid'])) the_file = os.path.join(the_dir, file_tag) the_id_file = the_file + '-id' # This shouldn't really happen as a file which is no-longer shared # doesn't have its symlink removed until the file manager is # refreshed. if not os.path.exists(the_file): logging.warn('File "%s" has vanished (%s)' % \ (the_file, info['login_name'])) continue if not os.path.exists(the_id_file): logging.warn('The id file "%s" has vanished (%s)' % \ (the_id_file, info['login_name'])) continue verb = 'Deleted' try: # Metadata files are renamed: they are small and keep # a history of what was there. This is only done for # files the user actually owns. if not os.path.islink(the_file): os.rename(the_id_file, the_id_file + '-removed') else: prefs = manage_kbasix._account_info(info['login_name'], \ 'prefs') # One-to-one shares are also symlinks, but when deleted # those are not hidden "never to be seen again" (i.e. they # can be re-shared), but instead just deleted. # We can tell if the share is of type GID/local by # inspecting the symlink prefix: shared_rpath = os.path.relpath(info['shared_dir_'], the_dir) if os.readlink(the_file).startswith(shared_rpath) and \ file_tag not in \ prefs['file_manager']['hidden_gidloc_shared_files']: verb = 'Hid' prefs['file_manager']['hidden_gidloc_shared_files'].append( file_tag) os.remove(the_id_file) manage_kbasix._account_mod(info['login_name'], 'prefs', \ prefs) os.remove(the_file) except Exception as reason: raise DeleteFileError('Unable to delete file "%s" because \ "%s" (%s)' % (the_file, reason, info['login_name'])) logging.debug('%s file "%s" (%s)' % \ (verb, the_file, info['login_name'])) return _initialize(req, info)
def _initialize(req, info): """Initialize the file manager page. _initialize(req, info) Returns the KBasix file manager page. """ import logging import aux import cgi import manage_kbasix logging.debug('Starting the file manager (%s)' % info['login_name']) info['user_dir_size'] = aux._bytes_string(aux._get_dir_size(info)) info['quota'] = aux._bytes_string(info['quota']) # Check to see if the files have been bulk-selected. if 'toggle_select' in req.form and \ req.form['toggle_select'].value == 'checked': info['toggle_select'] = 'checked' else: info['toggle_select'] = '' # The file manager settings are stored in the user's preferences file. prefs = manage_kbasix._account_info(info['login_name'], 'prefs') # Default file manager settings. if 'file_manager' not in prefs: logging.debug('Setting first-time preferences (%s)' % \ info['login_name']) prefs['file_manager'] = {} prefs['file_manager']['hidden_gidloc_shared_files'] = [] prefs['file_manager']['sort_criteria'] = \ info['default_sort_criteria_'] prefs['file_manager']['reverse'] = '' prefs['file_manager']['hide_shared'] = '' prefs['file_manager']['condensed_view'] = '' prefs['file_manager']['keywords'] = '' # Update the settings if filtering has been requested. for key in prefs['file_manager']: if not info['filter']: continue # This key is not directly changed by editing form values, but by # deleting shared entries. if key == 'hidden_gidloc_shared_files': continue if key in req.form: # We should use html.escape when migrating to python3 val = cgi.escape(req.form[key].value, True) if key == 'keywords': # If the keywords are not decoded here there are problems # substituting a mix of unicode and str e.g.: # # return '<html><body><p>%s</p><p>%s</p></body></html>' % \ # (info['main_header'], prefs['file_manager']['keywords']) # # fails. val = val.decode('utf-8') elif val and val \ not in info['allowed_sort_criteria'] + ['checked']: val = '' prefs['file_manager'][key] = val # This enables toggling. elif key not in req.form and prefs['file_manager'][key]: prefs['file_manager'][key] = '' # If someone tried to inject an invalid "sort_criteria" it'd be blanked # out above, and we fall back on "default_sort_criteria_" (invalid # radio buttons are already blanked out). if not prefs['file_manager']['sort_criteria']: prefs['file_manager']['sort_criteria'] = \ info['default_sort_criteria_'] info['sort_criteria_list'] = '\n' # Create the sort criteria drop. for i in info['allowed_sort_criteria']: s = '' if i == prefs['file_manager']['sort_criteria']: s = 'selected' info['sort_criteria_list'] += \ '<option %s value="%s">%s</option>\n' % \ (s, i, i.split('_')[-1].capitalize()) manage_kbasix._account_mod(info['login_name'], 'prefs', prefs) info['file_list'] = _get_file_list(info) info.update(prefs['file_manager']) return aux._fill_page(info['file_manager_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 process(req): """Process the upload page. process(req) """ # Note that upload sizes can be configured via "LimitRequestBody" # either in .htaccess or httpd.conf (the latter requires an httpd # restart). However, doing so results in mod_python spitting out a # nasty error message ("Request Entity Too Large") upon hitting said # limit. See: # # http://www.mail-archive.com/[email protected]/msg87503.html # # We can somewhat gracefully bypass this behaviour by adding the # following to .htaccess (using 10485760 as an example, and keeping in # mind that on a production server PythonDebug should be Off in # python.conf): # # PythonDebug Off # LimitRequestBody 10485760 # ErrorDocument 413 /path/upload.py/process?file_limit=10485760 # ErrorDocument 500 "An error was encountered" # # Note that the ErrorDocument message should be small (see below), and # that the paths are relative to DocumentRoot. # # Now, this approach has the following issue: the file is _not_ # uploaded to the server, but instead seems to be stored on the client # side and for some reason takes a long time to process there # (strangely so, as it's not being transferred). This method seems to # work well for medium-sized files above LimitRequestBody, but then for # huge files the client shows a connection reset error, explained here: # # http://stackoverflow.com/questions/4467443/limitrequestbody-doesnt-respond-with-413-for-large-file25mb # # In spite of the fact that the client feedback is slow (and that the # connection is reset on large files) it may be worthwhile to keep # since at least this does limit the resources used on the server, # including avoiding the "phantom file" space usage on /tmp (downloads # are stored in /tmp as a "phantom file", its usage can be obtained via # "df", freeing it requires an httpd restart if the upload happens to # die in one of the "file too large" manners [just closing the tab does # not cause this]). # # In short: # # 0 < size < LimitRequestBody: Upload success ful (OK) # LimitRequestBody < size < ~1GB : Upload limit message # (OK) # ~1GB < size < ~2GB : Connection reset (file # too large) # ~2GB < size : Browsers barf (file # too large) from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page, _fill_str, _go_back_button from defs import kbasix, upload info = {} info.update(kbasix) info.update(upload) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for upload.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) # The following 'file_limit' comes from the 413 error redirect in # .htaccess. We disallow sneakiness by making sure the only argument # is an integer. Furthermore we note that no session handling is done # by this point (so, although the error page has no tokens, the browser # "Back" button will take us to our upload page, regardless of whether # the tokens are per-request). Since it's a dead-end error page with # no user-data it shouldn't affect the fact it lives outside a session's # scope. # Although the uploads page is session-protected the upload-limit error # message has to be public as we can't get the token from # .htaccess. Furthermore, it seems this message has to be pretty small # (returning "status_page_" gives "Request Entity Too Large"). Note # that the user temporarily loses their token, but has no other option # other than to browse "Back", recovering their token (also important # because HTTPS isn't required either). if 'file_limit' in req.form: file_limit = req.form['file_limit'].value try: max_size = int(file_limit) except: info['details'] = 'Non-numerical upload size' logging.error(info['details'] + ': %s' % file_limit) return _fill_page(info['error_page_'], info) info['details'] = 'The file you are trying to upload is too \ large (max size = %s bytes).' % max_size logging.warn(info['details']) return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot upload over an insecure connection.' logging.info(info['details']) return _fill_page(info['error_page_'], info) try: session = _is_session(req, required=True) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) # User cannot upload if over-quota. try: (info['user_dir_size'], over_page) = \ _check_quota(req, 'upload', info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to determine quota [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if info['user_dir_size'] < 0: return over_page if 'start' in req.form: try: return _initialize(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize upload [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified trying to upload.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'upload_file': try: return _get_file(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Error uploading file [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected action trying to upload.' logging.error(info['details']) return _fill_page(info['error_page_'], info)
def process(req): """Process the login page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, login info = {} info.update(kbasix) info.update(login) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for login.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot login over an insecure connection.' logging.info('Disallowed insecure access to login.py') return _fill_page(info['error_page_'], info) try: session = _is_session(req, required=False) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) # This page should not appear to people who are already logged in. if info['token']: info['details'] = 'You are already logged in as "%s".' % \ info['user_name'] logging.debug(info['details']) return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) # When a page requires a user to be logged-in it sends its name as # "referrer", which is obtained from "req.canonical_filename", and # spoofing it shouldn't gain anything other than attempting to access # a different URL (which can be done by editing the URL directly # anyways). if 'referrer' not in req.form: info['referrer'] = 'main.py' else: info['referrer'] = req.form['referrer'].value if 'start' in req.form: try: return _initialize(info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to open login page [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified trying to login.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'login': try: if not req.form['user_name']: info['details'] = 'You must input your user name.' return _fill_page(info['error_page_'], info) else: info['user_name'] = req.form['user_name'].value info['login_name'] = info['user_name'].lower() return _login(req, info) except SendTokenError as reason: logging.critical(reason) info['details'] = '[SYS] Unable to email new token [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) except LoginError as reason: logging.critical(reason) info['details'] = '[SYS] Unable to login [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to login [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected action trying to login.' logging.error(info['details']) return _fill_page(info['error_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 process(req): """Process the profile page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, profile info = {} info.update(kbasix) info.update(profile) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for profile.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot edit your profile over an insecure \ connection.' return _fill_page(info['error_page_'], info) try: # We need to holdover (which only takes when "per_request_token" is # True) because we want to keep the restricted token generated when # a password is forgotten. Note that this check should be added to # any module which needs to be restricted. if 'token' in req.form and '-all-tk' not in req.form['token']: session = _is_session(req, required=True, holdover=True) else: session = _is_session(req, required=True) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) if 'start' in req.form: try: return _initialize(info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize profile editor \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified trying to edit profile.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'update': try: return _update(req, info) except UpdateError as reason: logging.critical(reason) info['details'] = '[SYS] Unable to update the profile \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to update the profile [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected action trying to update the profile.' logging.error(info['details']) return _fill_page(info['error_page_'], info)
def _delete_file(req, info): """Delete/hide a file and its respective metadata. _delete_file(req, info) Returns to the file manager page. """ import logging import os import manage_kbasix if 'file_tag' in req.form: file_tags = [req.form['file_tag'].value] elif 'file_tags' in req.form: file_tags_str = req.form['file_tags'].value file_tags = file_tags_str.split() for file_tag in file_tags: _check_file_tag(file_tag, info['login_name']) the_dir = os.path.join(info['users_root_dir_'], str(info['uid'])) the_file = os.path.join(the_dir, file_tag) the_id_file = the_file + '-id' # This shouldn't really happen as a file which is no-longer shared # doesn't have its symlink removed until the file manager is # refreshed. if not os.path.exists(the_file): logging.warn('File "%s" has vanished (%s)' % \ (the_file, info['login_name'])) continue if not os.path.exists(the_id_file): logging.warn('The id file "%s" has vanished (%s)' % \ (the_id_file, info['login_name'])) continue verb = 'Deleted' try: # Metadata files are renamed: they are small and keep # a history of what was there. This is only done for # files the user actually owns. if not os.path.islink(the_file): os.rename(the_id_file, the_id_file + '-removed') else: prefs = manage_kbasix._account_info(info['login_name'], \ 'prefs') # One-to-one shares are also symlinks, but when deleted # those are not hidden "never to be seen again" (i.e. they # can be re-shared), but instead just deleted. # We can tell if the share is of type GID/local by # inspecting the symlink prefix: shared_rpath = os.path.relpath(info['shared_dir_'], the_dir) if os.readlink(the_file).startswith(shared_rpath) and \ file_tag not in \ prefs['file_manager']['hidden_gidloc_shared_files']: verb = 'Hid' prefs['file_manager']['hidden_gidloc_shared_files'].append(file_tag) os.remove(the_id_file) manage_kbasix._account_mod(info['login_name'], 'prefs', \ prefs) os.remove(the_file) except Exception as reason: raise DeleteFileError('Unable to delete file "%s" because \ "%s" (%s)' % (the_file, reason, info['login_name'])) logging.debug('%s file "%s" (%s)' % \ (verb, the_file, info['login_name'])) return _initialize(req, info)
def _initialize(req, info): """Initialize the file manager page. _initialize(req, info) Returns the KBasix file manager page. """ import logging import aux import cgi import manage_kbasix logging.debug('Starting the file manager (%s)' % info['login_name']) info['user_dir_size'] = aux._bytes_string(aux._get_dir_size(info)) info['quota'] = aux._bytes_string(info['quota']) # Check to see if the files have been bulk-selected. if 'toggle_select' in req.form and \ req.form['toggle_select'].value == 'checked': info['toggle_select'] = 'checked' else: info['toggle_select'] = '' # The file manager settings are stored in the user's preferences file. prefs = manage_kbasix._account_info(info['login_name'], 'prefs') # Default file manager settings. if 'file_manager' not in prefs: logging.debug('Setting first-time preferences (%s)' % \ info['login_name']) prefs['file_manager'] = {} prefs['file_manager']['hidden_gidloc_shared_files'] = [] prefs['file_manager']['sort_criteria'] = \ info['default_sort_criteria_'] prefs['file_manager']['reverse'] = '' prefs['file_manager']['hide_shared'] = '' prefs['file_manager']['condensed_view'] = '' prefs['file_manager']['keywords'] = '' # Update the settings if filtering has been requested. for key in prefs['file_manager']: if not info['filter']: continue # This key is not directly changed by editing form values, but by # deleting shared entries. if key == 'hidden_gidloc_shared_files': continue if key in req.form: # We should use html.escape when migrating to python3 val = cgi.escape(req.form[key].value, True) if key == 'keywords': # If the keywords are not decoded here there are problems # substituting a mix of unicode and str e.g.: # # return '<html><body><p>%s</p><p>%s</p></body></html>' % \ # (info['main_header'], prefs['file_manager']['keywords']) # # fails. val = val.decode('utf-8') elif val and val \ not in info['allowed_sort_criteria'] + ['checked']: val = '' prefs['file_manager'][key] = val # This enables toggling. elif key not in req.form and prefs['file_manager'][key]: prefs['file_manager'][key] = '' # If someone tried to inject an invalid "sort_criteria" it'd be blanked # out above, and we fall back on "default_sort_criteria_" (invalid # radio buttons are already blanked out). if not prefs['file_manager']['sort_criteria']: prefs['file_manager']['sort_criteria'] = \ info['default_sort_criteria_'] info['sort_criteria_list'] = '\n' # Create the sort criteria drop. for i in info['allowed_sort_criteria']: s = '' if i == prefs['file_manager']['sort_criteria']: s = 'selected' info['sort_criteria_list'] += \ '<option %s value="%s">%s</option>\n' % \ (s, i, i.split('_')[-1].capitalize()) manage_kbasix._account_mod(info['login_name'], 'prefs', prefs) info['file_list'] = _get_file_list(info) info.update(prefs['file_manager']) return aux._fill_page(info['file_manager_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 _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 process(req): """Process the file manager page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, file_manager info = {} info.update(kbasix) info.update(file_manager) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for file_manager.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot manage your file over an insecure \ connection.' logging.info('Disallowed insecure access to file_manager.py') return _fill_page(info['error_page_'], info) try: # The holdover is required if "per_request_token" is True, and # has no effect otherwise. This is needed due to the # client-generated download window (which has no concept of the # new request token). if 'action' in req.form and req.form['action'] == 'download': session = _is_session(req, required=True, holdover=True) else: session = _is_session(req, required=True) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) info['filter'] = False if 'start' in req.form: try: return _initialize(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize the file manager \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified within the file manager.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'filter': try: info['filter'] = True return _initialize(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] File manager error [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'copy_file': try: return _copy_file(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] File manager error [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'confirm_delete': try: return _confirm_delete(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] File manager error [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'delete': try: return _delete_file(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Error deleting file [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'confirm_bulk_delete': try: return _confirm_bulk_delete(req, info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Error deleting files [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) elif req.form['action'] == 'download': try: return _download_file(req, info) except Exception as reason: # Not critical... user-end problem maybe? logging.error(reason) info['details'] = '[SYS] Unable to download file [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected file manager action.' logging.error(info['details']) return _fill_page(info['error_page_'], info)
def process(req): """Process the profile page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, profile info = {} info.update(kbasix) info.update(profile) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for profile.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot edit your profile over an insecure \ connection.' return _fill_page(info['error_page_'], info) try: # We need to holdover (which only takes when "per_request_token" is # True) because we want to keep the restricted token generated when # a password is forgotten. Note that this check should be added to # any module which needs to be restricted. if 'token' in req.form and '-all-tk' not in req.form['token']: session = _is_session(req, required=True, holdover=True) else: session = _is_session(req, required=True) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) if 'start' in req.form: try: return _initialize(info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize profile editor \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified trying to edit profile.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'update': try: return _update(req, info) except UpdateError as reason: logging.critical(reason) info['details'] = '[SYS] Unable to update the profile \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to update the profile [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected action trying to update the profile.' logging.error(info['details']) return _fill_page(info['error_page_'], info)
def process(req): """Process the registration page. process(req) """ from manage_kbasix import _is_session, _account_info from aux import _make_header, _fill_page from defs import kbasix, register info = {} info.update(kbasix) info.update(register) import logging logging.basicConfig(level = getattr(logging, \ info['log_level_'].upper()), \ filename = info['log_file_'], \ datefmt = info['log_dateformat_'], \ format = info['log_format_']) if repr(type(req)) != "<type 'mp_request'>": logging.critical('Invalid request for register.py') info['details'] = '[SYS] Invalid request [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) if not req.is_https(): info['details'] = 'You cannot register over an insecure connection.' logging.info('Disallowed insecure access to register.py') return _fill_page(info['error_page_'], info) try: session = _is_session(req, required=False) info.update(session) if session['token']: profile = _account_info(info['login_name'], 'profile') info.update(profile) except Exception as reason: logging.warn(reason) info['details'] = '[SYS] Unable to verify session [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) info['main_header'] = _make_header(info) if 'start' in req.form: try: return _initialize(info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to initialize registration \ [%s].' % info['error_blurb_'] return _fill_page(info['error_page_'], info) elif 'action' not in req.form: info['details'] = 'No action specified trying to register.' logging.warn(info['details']) return _fill_page(info['error_page_'], info) elif req.form['action'] == 'register': try: return _register(req, info) except RegisterError as reason: logging.critical(reason) info['details'] = '[SYS] Unable to register [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) except Exception as reason: logging.critical(reason) info['details'] = '[SYS] Unable to register [%s].' % \ info['error_blurb_'] return _fill_page(info['error_page_'], info) else: info['details'] = 'Unexpected action trying to register.' logging.error(info['details']) return _fill_page(info['error_page_'], info)
def _get_shares(file_info, info): """Convert UIDs/GIDs/boolean shares as stored in the file metadata into actual user names/group names/HTML 'checked' (or not). (users, groups, local, world) = _get_shares(file_info, info) Returns a (list, list, str, str) tuple. """ import logging import os import manage_kbasix import manage_users id_file = file_info['file_tag'] + '-id' users = [] for i in file_info['uid_shares']: user_info = manage_users._info(i) # If 'remove_exusers_shares' is False we'll try to keep # the shares of non-existent users (so that if they return # they can recover said shares). However, this doesn't work # well in practice and the metadata editor might wipe it out # during an update. Furthermore, the user might be confused # when seeing '*.7' in the list. Same for 'remove_exgroups_shares'. if not user_info: if not info['remove_exusers_shares']: users.append('*.%s' % i) else: # Users who have deleted the file being shared with them # show up with a '-' prefix in the shared user list. user_name = \ manage_kbasix._account_info(user_info['login_name'], \ 'profile')['user_name'] if not os.path.exists(os.path.join(info['users_root_dir_'], \ str(user_info['uid']), \ id_file)): users.append('-' + user_name) else: users.append(user_name) if users: users = ', '.join(users) else: users = '' groups = [] for i in file_info['gid_shares']: group_info = manage_users._info(i, is_type='group') if not group_info: if not info['remove_exgroups_shares']: groups.append('*.%s' % i) else: groups.append(group_info['group_name']) if groups: groups = ', '.join(groups) else: groups = '' # The booleans become 'checked' if True, '' otherwise. if file_info['local_share']: local = 'checked' else: local = '' if file_info['world_share']: world = 'checked' else: world = '' return (users, groups, local, world)