def redirect(url='', internal=True, debug=False): """Raise InternalRedirect or HTTPRedirect to the given url.""" if debug: cherrypy.log('Redirecting %sto: %s' % ({True: 'internal ', False: ''}[internal], url), 'TOOLS.REDIRECT') if internal: raise cherrypy.InternalRedirect(url) else: raise cherrypy.HTTPRedirect(url)
def browse_catalog(self, category=None, category_sort=None): 'Entry point for top-level, categories and sub-categories' prefix = '' if self.is_wsgi else self.opts.url_prefix if category is None: ans = self.browse_toplevel() # The following are fake categories used for the top-level view elif category == 'newest': raise cherrypy.InternalRedirect(prefix + '/browse/matches/newest/dummy') elif category == 'allbooks': raise cherrypy.InternalRedirect(prefix + '/browse/matches/allbooks/dummy') elif category == 'randombook': raise cherrypy.InternalRedirect(prefix + '/browse/random') else: ans = self.browse_category(category, category_sort) return ans
def check_session(self, logging_in=False): """ Overwrite the BasePage's check_session function to make sure that this user is an admin ... """ # Call the base session checker BasePage.check_session(self, logging_in) if not self.has_min_perms(perms.DEITY): raise cherrypy.InternalRedirect("/denied")
def add(self,add,description,duedate): username = logon.checkauth() taskdir = gettaskdir(username) filename = os.path.join(taskdir,uuid().hex+'.task') d=configparser() d.add_section('task') d.set('task','description',description) d.set('task','duedate',duedate) with open(filename,"w") as file: d.write(file) raise cherrypy.InternalRedirect(".")
def checkValidatorsRights(self): if not self.check_validator(): raise cherrypy.InternalRedirect('/permissionErrorMessage') cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) request = simplejson.loads(rawbody) cherrypy.response.headers['Content-Type'] = 'application/json' return simplejson.dumps([ checkValidatorRights(request["cat"], request["subCategory"], request["statusKind"], self.get_username(), Session) ])
def _error_handler(url='/error', internal=True): """Raise InternalRedirect or HTTPRedirect to the given url.""" #cherrypy.log("CherryPy %s error (%s) for request '%s'" % (status, error_msg, url), traceback=True) ##send_exception_email() if internal: url = "%s?%s" % (url, urllib.urlencode(dict(url=cherrypy.url()))) raise cherrypy.InternalRedirect(url) else: raise cherrypy.HTTPRedirect(url)
def command(self, whichone, what): # first change the light state # toggle(whichone) # now reload the index page to tell the user print "incoming with ", whichone, what if (what.lower() == 'on'): switchOn(whichone) if (what.lower() == "off"): switchOff(whichone) if (what.lower() == "toggle"): switchToggle(whichone) raise cherrypy.InternalRedirect('/index')
def update_story(self, story_title="", story_body="", filename=""): if story_title == "" or story_body == "" or filename == "": raise cpy.InternalRedirect("ls") filename = os.path.join(config('datadir'), filename) tmpfile = filename + ".bak" try: f = open(tmpfile, 'w') f.write(story_title + "\n") f.write(htmlunescape(story_body)) except Exception, e: os.unlink(tmpfile) cpy.log("unable to log: " + e.Message)
def oauth_dance(self, **kwargs): """ Performs oauth dance for the supported services with helper functions located in servicewrapper. """ if len(kwargs) != 0: self._services.update(kwargs) if 'user' in self._services: authtoken = dbwrapper.add_user(self._services['user']) cherrypy.session['tokens'] = {} cherrypy.session['tokens']['user'] = self._services['user'] cherrypy.session['tokens']['authtoken'] = authtoken # delete user from the dict because this method is called again # in the oauth dance loop adn we don't want to add user again if 'user' in self._services: del self._services['user'] cherrypy.lib.sessions.save() # get the Oauth urls for every service if len(self._services['Twitter']) > 1: url, key, secret = servicewrapper.twitter_get_oauth_url( self._callback_url) cherrypy.session['tokens']['twitter_key'] = key cherrypy.session['tokens']['twitter_secret'] = secret raise cherrypy.HTTPRedirect(url) if len(self._services['Foursquare']) > 1: url = servicewrapper.foursquare_get_oauth_url(self._callback_url) raise cherrypy.HTTPRedirect(url) if len(self._services['Facebook']) > 1: # Facebook module does not support Oauth so it's implemented here app_id, temp = dbwrapper.get_service_tokens('Facebook') args = dict(client_id=app_id, redirect_uri=self._callback_url, scope='publish_actions') raise cherrypy.HTTPRedirect( "https://www.facebook.com/dialog/oauth?" + urllib.urlencode(args)) raise cherrypy.InternalRedirect('success')
def post(self, link='', title='', tags_string=''): tags_string = sp2sl(tags_string) if link: db.store(link, title, tags_string) raise cherrypy.InternalRedirect(tags_string) #tmpl = lookup.get_template("index.html") #links = db.retrieve(tags_string) #all_links = '' #for link in links: # all_links = all_links + '<p>' + link + '</p>' #return tmpl.render(title="TITLE", body=all_links) else: tmpl = lookup.get_template("post.html") return tmpl.render(title="TITLE")
def sslcert_newcert_rtn(self, **params): #pprint.pprint(params) # Todo: add some validation self.auth.authorize() rtn = self.certobj.gen_server_cert(params, ip_lst=params['ip_lst'], dns_lst=params['dns_lst']) if rtn == True: rtn = os.system("(sleep 2; systemctl restart nginx)&") raise cherrypy.InternalRedirect('/webpanel/sslcert/') else: raise cherrypy.HTTPError(500, self.certobj.error_msg)
def submit(self, login, email): session=cherrypy.request.db user=data.User.get_by_login(session, login) if not( user != None and user.e_mail==email and (user.status=='OK' or user.status=='FORGOTTEN')): cherrypy.session['message']="User with these credentials does not exist." raise cherrypy.InternalRedirect("/login/forgotten/") user.status='FORGOTTEN' user.uuid=uuid.uuid1().hex # user.password='' util.email(email,'system/email/forgotten.genshi',newlogin=login, uuid=user.uuid) return util.render('system/msg/forgotten.genshi',newlogin=login)
def submit(self, login, oldpass, newpass, verpass): session=cherrypy.request.db user=data.User.get_by_login(session, login) if login != get_login() or user == None: clear_session() # cherrypy.session cherrypy.session['message']="Error. Please log again." raise cherrypy.InternalRedirect("/login/") # if login == 'admin': # cherrypy.session['message']="Changing administrator password not allowed on WeBIAS demo site." # raise cherrypy.InternalRedirect("/login/passwd/") if newpass!=verpass: cherrypy.session['message']="Passwords do not match." raise cherrypy.InternalRedirect("/login/passwd/") if not user.authenticate(oldpass): cherrypy.session['message']="Wrong password." raise cherrypy.InternalRedirect("/login/passwd/") user.update_passwd(newpass) util.go_back()
def checkauth(logonurl="/", returntopage=False): returnpage = '' if returntopage: returnpage = '?returnpage=' + cherrypy.request.script_name + cherrypy.request.path_info #returnpage='?returnpage='+cherrypy.request.base+cherrypy.request.script_name+cherrypy.request.path_info #returnpage='?returnpage='+cherrypy.request.path_info print(cherrypy.request.params) if (len(cherrypy.request.params)): returnpage += "?" + "&".join( "%s=%s" % t for t in cherrypy.request.params.items()) auth = cherrypy.session.get('authenticated', None) if auth == None: raise cherrypy.InternalRedirect(logonurl + returnpage) return auth
def check(self, **kwargs): request = cherrypy.serving.request path = request.path_info if request.path_info == '/' else request.path_info.rstrip( '/') mount_point = _config('controller_path', '/auth') request._auth_user = _config('user_class', BaseUser)() if path.startswith(mount_point) or request._auth_user.check_rights( path): return False if request._auth_user.is_guest(): raise cherrypy.HTTPRedirect(mount_point + '/logon') raise cherrypy.InternalRedirect(mount_point + '/denied', urlencode({'path': path}))
def __init__(self, errors): """Set up identity errors on the request and get URL from config.""" set_identity_errors(errors) url = get_failure_url(errors) # We need forward URL for both external and internal redirects params = cherrypy.request.params params['forward_url'] = cherrypy.request.path_info if gearshift.config.get('tools.identity.force_external_redirect', False): # We need to use external redirect for https since we are managed # by Apache/nginx or something else that CherryPy won't find. raise cherrypy.HTTPRedirect(gearshift.url(url, params)) else: # use internal redirect which is quicker query_string = urllib.urlencode(params, True) raise cherrypy.InternalRedirect(url, query_string)
def mark(self,id,duedate,description,completed,done=None,delete=None): username = logon.checkauth() taskdir = gettaskdir(username) filename = os.path.join(taskdir,id+'.task') if done=="Done": print('####',id,duedate,description,completed,done,delete) d=configparser() with open(filename,"r") as file: d.readfp(file) if completed == "" or completed == "None": completed = date.today().isoformat() d.set('task','completed',completed) with open(filename,"w") as file: d.write(file) elif delete=="Del": os.unlink(filename) raise cherrypy.InternalRedirect(".")
def process_group_form(self, **kw): '''Process the group add or edit form and do the DB transactions @param kw: a dictionary containing name, description and gid (gid=0 if adding new record) ''' # Confirm user authentication self.check_session() if int(kw['gid']) == 0: # We're adding a new group self.webservice.add_group({ 'name' : kw['name'], 'description' : kw['description']}) else: # We're updating a group self.webservice.edit_group( { 'gid' : kw['gid'], 'name' : kw['name'], 'description' : kw['description']}) raise cherrypy.InternalRedirect("/admin/groups")
def salt_auth_tool(): ''' Redirect all unauthenticated requests to the login page ''' # Short-circuit for the login page ignore_urls = ('/login',) if cherrypy.request.path_info.startswith(ignore_urls): return # Otherwise redirect to the login page if the session hasn't been authed if not cherrypy.session.get('token', None): raise cherrypy.InternalRedirect('/login') # Session is authenticated; inform caches cherrypy.response.headers['Cache-Control'] = 'private'
def execute_analysis(self, query_args): # Analyze the URL. analysis = analyze_url(cherrypy.request.path_info) directive = analysis.directive content = analysis.content # If any "directives" were found (i.e., redirections) perform them here. if directive is not None: if directive.type == Directive.HTTPRedirect: raise cherrypy.HTTPRedirect(analysis.directive.argument) elif directive.type == Directive.InternalRedirect: raise cherrypy.InternalRedirect(analysis.directive.argument) elif directive.type == Directive.ListPlugins: tangelo.content_type("application/json") plugin_list = self.plugins.plugin_list() if self.plugins else [] return json.dumps(plugin_list) else: raise RuntimeError("fatal internal error: illegal directive type code %d" % (analysis.directive.type)) # If content was actually found at the URL, perform any htaccess updates # now. do_auth = self.auth_update and content is None or content.type != Content.NotFound if do_auth: self.auth_update.update(analysis.reqpathcomp, analysis.pathcomp) # Serve content here, either by serving a static file, generating a # directory listing, executing a service, or barring the client entry. if content is not None: if content.type == Content.File: if content.path is not None: return cherrypy.lib.static.serve_file(content.path) else: raise cherrypy.HTTPError("403 Forbidden", "The requested path is forbidden") elif content.type == Content.Directory: if content.path is not None: return Tangelo.dirlisting(content.path, cherrypy.request.path_info) else: raise cherrypy.HTTPError("403 Forbidden", "Listing of this directory has been disabled") elif content.type == Content.Service: cherrypy.thread_data.pluginpath = analysis.plugin_path return self.invoke_service(content.path, *content.pargs, **query_args) elif content.type == Content.NotFound: raise cherrypy.HTTPError("404 Not Found", "The path '%s' was not found" % (content.path)) else: raise RuntimeError("fatal error: illegal content type code %d" % (content.type)) else: raise RuntimeError("fatal internal error: analyze_url() returned analysis without directive or content")
def success(self, *args, **kwargs): """ Redirects from external services return here one by one. If tokens are verified and fetched successfully, user data will be added and updated accordingly. """ parameters = cherrypy.request.params if len(self._services['Twitter']) > 1: self._services['Twitter'] = '' if 'oauth_verifier' not in parameters: raise cherrypy.InternalRedirect('oauth_dance') self._statuses['twt'] = servicewrapper.twitter_save_access_token( cherrypy.session['tokens']['authtoken'], cherrypy.session['tokens']['twitter_key'], cherrypy.session['tokens']['twitter_secret'], parameters['oauth_verifier']) raise cherrypy.InternalRedirect('oauth_dance') if len(self._services['Foursquare']) > 1: self._services['Foursquare'] = '' if 'code' not in parameters: raise cherrypy.InternalRedirect('oauth_dance') self._statuses['fq'] = servicewrapper.foursquare_save_access_token( cherrypy.session['tokens']['authtoken'], parameters['code'], self._callback_url) raise cherrypy.InternalRedirect('oauth_dance') if len(self._services['Facebook']) > 1: self._services['Facebook'] = '' if 'error' in parameters: raise cherrypy.InternalRedirect('oauth_dance') self._statuses['fb'] = servicewrapper.facebook_save_access_token( cherrypy.session['tokens']['authtoken'], parameters['code'], self._callback_url) raise cherrypy.InternalRedirect('oauth_dance') data = dbwrapper.get_user_data(cherrypy.session['tokens']['authtoken']) del cherrypy.session['tokens'] return 'Added user %s. <br />Secret token (save this): <b>%s</b><br />\ Facebook: %s, Twitter: %s, Foursquare: %s' % ( data[1], data[2], self._statuses['fb'], self._statuses['twt'], self._statuses['fq'])
def add_story(self, story_title="", story_body="", filename=""): if story_title == "" or story_body == "" or filename == "": raise cpy.InternalRedirect("ls") filename = os.path.join(config('datadir'), filename) if not filename.endswith('.txt'): filename += '.txt' if os.path.isfile(filename): ns = {'title': 'File Already Exists', 'filename': filename} return [self.navbar(ns), ('admin_story_already_exists', ns)] try: f = open(filename, 'w') f.write(story_title + "\n") f.write(htmlunescape(story_body)) except Exception, e: cpy.log("unable to log: " + e.Message)
def hypermedia_handler(*args, **kwargs): ''' Determine the best output format based on the Accept header, execute the regular handler, and transform the output to the request content type (even if it's an error). :param args: Pass args through to the main handler :param kwargs: Pass kwargs through to the main handler ''' # If we're being asked for HTML, try to serve index.html from the 'static' # directory; this is useful (as a random, non-specific example) for # bootstrapping the salt-ui app index = os.path.join(cherrypy.config['static'], 'index.html') if 'html' in wants_html() and os.path.exists(index): return cherrypy.lib.static.serve_file(index) # Execute the real handler. Handle or pass-through any errors we know how # to handle (auth & HTTP errors). Reformat any errors we don't know how to # handle as a data structure. try: cherrypy.response.processors = dict(ct_out_map) # handlers may modify this ret = cherrypy.serving.request._hypermedia_inner_handler(*args, **kwargs) except salt.exceptions.EauthAuthenticationError: raise cherrypy.InternalRedirect('/login') except cherrypy.CherryPyException: raise except Exception as exc: logger.debug("Error while processing request for: %s", cherrypy.request.path_info, exc_info=True) cherrypy.response.status = 500 ret = { 'status': cherrypy.response.status, 'message': '{0}'.format(exc) if cherrypy.config['debug'] else "An unexpected error occurred"} # Raises 406 if requested content-type is not supported best = cherrypy.lib.cptools.accept([i for (i, _) in ct_out_map]) # Transform the output from the handler into the requested output format cherrypy.response.headers['Content-Type'] = best out = cherrypy.response.processors[best] return out(ret)
def removeUser(self): if not self.check_admin(): raise cherrypy.InternalRedirect('/permissionErrorMessage') cherrypy.response.headers['Content-Type'] = 'application/json' cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) request = simplejson.loads(rawbody) user_Name = request["user_Name"] returnInformation = removeUser(user_Name, Session) if returnInformation == "True": info = "User " + user_Name + " was removed successfuly" cherrypy.response.headers['Content-Type'] = 'application/json' logging.info("%s %s", self.get_fullname(), info) return simplejson.dumps([info]) else: info = "User " + user_Name + " was not removed because - " + returnInformation cherrypy.response.headers['Content-Type'] = 'application/json' logging.error("%s %s", self.get_fullname(), info) return simplejson.dumps([info])
def cred_crud_rtn(self, **parms): trex = TemplateRex(fname='t_loginform_crud.html') # ---- Validate Input ---------------- parms['msg'] = self.check_credentials(parms['username'], parms['password']) if parms['msg'] != True: return (trex.render(parms)) if not (parms['username_new'] or parms['username_verify'] or parms['password_new'] or parms['password_verify']): parms['msg'] = "Blank Username or Password" return (trex.render(parms)) if (parms['username_new'] != parms['username_verify']) or ( parms['password_new'] != parms['password_verify']): parms[ 'msg'] = "New Username or Password do not mach Verify Username or Password" return (trex.render(parms)) # Looks good go create new file. Note only allowng one user at this point in time. # Multiple user only makes sense when there are roles self.rw() ht = HtpasswdFile(self.htpasswd, new=True) ht.set_password(parms['username_new'], parms['password_new']) rtn = ht.save() self.ro() if not 'from_page' in parms: parms['from_page'] = '/' get_parms = { 'from_page': parms['from_page'], 'username': parms['username_new'], 'password': parms['password_new'] } query_str = urllib.parse.urlencode(get_parms) raise cherrypy.InternalRedirect(self.url_login, query_str)
def iredir(self): raise cherrypy.InternalRedirect('/storage_type')
def iredir(self): raise cherrypy.InternalRedirect('/blah')
def ajax_category(self, name, sort='title', num=100, offset=0, sort_order='asc'): ''' Return a dictionary describing the category specified by name. The dictionary looks like:: { 'category_name': Category display name, 'base_url': Base URL for this category, 'total_num': Total numberof items in this category, 'offset': The offset for the items returned in this result, 'num': The number of items returned in this result, 'sort': How the returned items are sorted, 'sort_order': asc or desc 'subcategories': List of sub categories of this category. 'items': List of items in this category, } Each subcategory is a dictionary of the same form as those returned by ajax_categories(). Each item is a dictionary of the form:: { 'name': Display name, 'average_rating': Average rating for books in this item, 'count': Number of books in this item, 'url': URL to get list of books in this item, 'has_children': If True this item contains sub categories, look for an entry corresponding to this item in subcategories in the main dictionary, } :param sort: How to sort the returned items. Choices are: name, rating, popularity :param sort_order: asc or desc To learn how to create subcategories see http://manual.calibre-ebook.com/sub_groups.html ''' try: num = int(num) except: raise cherrypy.HTTPError(404, "Invalid num: %r" % num) try: offset = int(offset) except: raise cherrypy.HTTPError(404, "Invalid offset: %r" % offset) base_url = absurl(self.opts.url_prefix, '/ajax/category/' + name) if sort not in ('rating', 'name', 'popularity'): sort = 'name' if sort_order not in ('asc', 'desc'): sort_order = 'asc' try: dname = decode_name(name) except: raise cherrypy.HTTPError( 404, 'Invalid encoding of category name' ' %r' % name) if dname in ('newest', 'allbooks'): if dname == 'newest': sort, sort_order = 'timestamp', 'desc' raise cherrypy.InternalRedirect( '/ajax/books_in/%s/%s?sort=%s&sort_order=%s' % (encode_name(dname), encode_name('0'), sort, sort_order)) fm = self.db.field_metadata categories = self.categories_cache() hierarchical_categories = self.db.prefs['categories_using_hierarchy'] subcategory = dname toplevel = subcategory.partition('.')[0] if toplevel == subcategory: subcategory = None if toplevel not in categories or toplevel not in fm: raise cherrypy.HTTPError(404, 'Category %r not found' % toplevel) # Find items and sub categories subcategories = [] meta = fm[toplevel] item_names = {} children = set() if meta['kind'] == 'user': fullname = ((toplevel + '.' + subcategory) if subcategory is not None else toplevel) try: # User categories cannot be applied to books, so this is the # complete set of items, no need to consider sub categories items = categories[fullname] except: raise cherrypy.HTTPError( 404, 'User category %r not found' % fullname) parts = fullname.split('.') for candidate in categories: cparts = candidate.split('.') if len(cparts) == len(parts) + 1 and cparts[:-1] == parts: subcategories.append({ 'name': cparts[-1], 'url': candidate, 'icon': category_icon(toplevel, meta) }) category_name = toplevel[1:].split('.') # When browsing by user categories we ignore hierarchical normal # columns, so children can be empty elif toplevel in hierarchical_categories: items = [] category_names = [ x.original_name.split('.') for x in categories[toplevel] if '.' in x.original_name ] if subcategory is None: children = set(x[0] for x in category_names) category_name = [meta['name']] items = [ x for x in categories[toplevel] if '.' not in x.original_name ] else: subcategory_parts = subcategory.split('.')[1:] category_name = [meta['name']] + subcategory_parts lsp = len(subcategory_parts) children = set( '.'.join(x) for x in category_names if len(x) == lsp + 1 and x[:lsp] == subcategory_parts) items = [ x for x in categories[toplevel] if x.original_name in children ] item_names = { x: x.original_name.rpartition('.')[-1] for x in items } # Only mark the subcategories that have children themselves as # subcategories children = set( '.'.join(x[:lsp + 1]) for x in category_names if len(x) > lsp + 1 and x[:lsp] == subcategory_parts) subcategories = [{ 'name': x.rpartition('.')[-1], 'url': toplevel + '.' + x, 'icon': category_icon(toplevel, meta) } for x in children] else: items = categories[toplevel] category_name = meta['name'] for x in subcategories: x['url'] = category_url(self.opts.url_prefix, x['url']) x['icon'] = icon_url(self.opts.url_prefix, x['icon']) x['is_category'] = True sort_keygen = { 'name': lambda x: sort_key(x.sort if x.sort else x.original_name), 'popularity': lambda x: x.count, 'rating': lambda x: x.avg_rating } items.sort(key=sort_keygen[sort], reverse=sort_order == 'desc') total_num = len(items) items = items[offset:offset + num] items = [{ 'name': item_names.get(x, x.original_name), 'average_rating': x.avg_rating, 'count': x.count, 'url': books_in_url(self.opts.url_prefix, x.category if x.category else toplevel, x.original_name if x.id is None else unicode(x.id)), 'has_children': x.original_name in children, } for x in items] return { 'category_name': category_name, 'base_url': base_url, 'total_num': total_num, 'offset': offset, 'num': len(items), 'sort': sort, 'sort_order': sort_order, 'subcategories': subcategories, 'items': items, }
def search(self, q=None, mac=None, name=None, address=None, content=None, success=False, **kw): ''' The search page where the search form POSTs ''' # Confirm user authentication self.check_session() # Initialization values = {} name_or_content = None if not (q or mac or name or content): raise cherrypy.InternalRedirect('/dns') if success: values['global_success'] = 'Records Updated Successfully!' def startswith(s, m): l = len(m) if s[:l] == m: return True return False def strip(s, m): return s[len(m):] if q: # Strip the query string and make sure it's a string q = str(q).strip().split('|') for i in q: if startswith(i, 'name:'): name = strip(i, 'name:') elif startswith(i, 'mac:'): mac = strip(i, 'mac:') elif startswith(i, 'content:'): content = strip(i, 'content:') elif startswith(i, 'address:'): address = strip(i, 'address:') elif startswith(i, 'addr:'): address = strip(i, 'addr:') elif startswith(i, 'ip:'): address = strip(i, 'ip:') else: name_or_content = i query_string = [] if name: query_string.append('name:%s' % name) if mac: query_string.append('mac:%s' % name) if address: query_string.append('address:%s' % name) if content: query_string.append('content:%s' % name) if name_or_content: query_string.append('%s' % name) values['query_string'] = '|'.join(query_string) values['dns_types_dropdown'] = self.webservice.get_dns_types({ 'only_useable': True, 'order_by': 'name' }) # Search by MAC if query is a MAC address if name_or_content and (name or content): raise Exception( "Cannot specify name or content with generic string matching.") if name_or_content: values['dns'] = self.get_dns(mac=mac, address=address, name=name_or_content) values['dns'] += self.get_dns(mac=mac, address=address, content=name_or_content) else: values['dns'] = self.get_dns(mac=mac, address=address, name=name, content=content) return self.__template.wrap(leftcontent=self.get_leftnav(), filename='%s/templates/dns.tmpl' % frontend.static_dir, values=values)
def index(self): #TODO: what's the right redirect to give here? raise cpy.InternalRedirect("/Admin/ls")