/",methods=['GET','POST'])
@requires_session
@requires_bid_access
def edit(user=None,groups=None,allowed=False,bid="",loggedin_uid=-1,busername="",b=None):
#print(f"DEBUG(bws) edit: locals()={locals()}")
print(f"DEBUG(bws) edit: user={user}")
print(f"DEBUG(bws) edit: groups={groups}")
print(f"DEBUG(bws) edit: allowed={allowed}")
print(f"DEBUG(bws) edit: bid={bid}")
print(f"DEBUG(bws) edit: loggedin_uid={loggedin_uid}")
print(f"DEBUG(bws) edit: busername={busername}")
print(f"DEBUG(bws) edit: b={b}")
# Common things for both POST and GET
ts = datetime.datetime.now().isoformat(timespec='seconds'),
if "" == bid or -1 == bid or "-1" == bid:
try:
return redirect(request.referer)
except:
return "",302
admin_message = ""
if busername != "":
# Then we allow it, with a warning
admin_message = f"Using admin access to edit bookmark for user \"{busername}\"."
if "GET" == request.method:
if allowed:
print(f"DEBUG: for rendering edit.html, aoint={b.always_open_in_new_tab}")
return render_template_extra("edit.html",
bid = b.bid,
url = b.url,
title = b.title,
tags = b.tags,
ts = b.timestamp,
notes = b.notes,
iid = b.iid,
order_id = b.order_id,
always_open_in_new_tab = b.always_open_in_new_tab,
admin_message = admin_message,
allowed = allowed
)
else:
# not allowed, from above logic
return f"Invalid bookmark.Returning momentarily...",401
elif "POST" == request.method:
f = request.form
# n is the new object dictionary, with the default values. Required fields are not listed here. Checkboxes are not listed here.
n = {
"title": "",
"tags": [],
"notes": "",
"iid": -1,
"timestamp": ts,
"order_id": -1
}
if 'draganddrop' in request.headers:
if "new" != bid:
return "Bad request",400
else:
# process the draganddrop stuff here
print(f"DEBUG(bws) edit/new/ d&d")
#for i in f:
# print(f"DEBUG(bws) edit/new/ d&d: Form[\"{i}\"]: \"{f[i]}\"")
if 'type' not in f:
return """Bad request: needs 'type' matching of these supported types: ['text/x-moz-place','text/x-moz-url','text/html']""",400
if 'string' not in f:
return "Bad request: no contents of 'string'!",400
ftype = f['type']
fstring = f['string']
if "text/x-moz-place" == ftype:
# item is a dict: title, id, itemGuid, instanceID, parent, parentGuid, dateAdded,lastModified,type,uri.
newitem = json.loads(fstring)
print(f"DEBUG(bws) edit/new/ d&d newitem={newitem}")
try: ftitle = newitem['title']
except: ftitle = n['title']
try: furl = newitem['uri']
except: return "Invalid request! No uri in text/x-moz-place object.",400
try: ftags = newitem['tags']
except: ftags = n['tags']
new_ftags = []
for i in ftags:
if str(i) != "":
new_ftags.append(i)
ftags = new_ftags
try: fnotes = newitem['comment']
except: fnotes = n['notes']
fts = convert_timestamp_from_epoch_to_useful(newitem['dateAdded'])
try: forder_id = newitem['id']
except: forder_id = 0
faoint = False
try:
fusername = f['username']
fuid = bws_db.get_uid_from_any(app.config['database'],fusername)
except:
fuid = loggedin_uid
b = bws_db.Bookmark(
source = app.config['database'],
url = furl,
title = ftitle,
uid = fuid,
tags = ftags,
notes = fnotes,
iid = 0,
timestamp = fts,
order_id = forder_id,
always_open_in_new_tab = faoint
)
if -1 == b.bid or "-1" == b.bid:
return "Something failed on server side.",500
else:
return f"Added bookmark id {b.bid}",201 # will get displayed in the drop box for 1.5 seconds by the javascript
return "OK",200
elif "text/x-moz-url" == ftype:
print(f"DEBUG(bws) still need support to parse text/x-moz-url \"{fstring}\"")
return "OK",200
elif "text/html" == ftype:
print(f"DEBUG(bws) still need support to parse text/html \"{fstring}\"")
else:
print(f"DEBUG(bws) e/n/ d&d: Unsupported type as of yet: \"{ftype}\" string \"{fstring}\"")
return f"Unsupported type as of yet: \"{ftype}\" string \"{fstring}\"",400
else: # not drag-and-drop
print(f"DEBUG(BWS): Got a POST for /edit/")
for i in f:
print(f"DEBUG(bws): Form[\"{i}\"]: \"{f[i]}\"")
# validate required fields
for word in ['url']:
if word not in f:
return f"Bad request: needs attribute \"{word}\".",400
# checkboxes are stupid, and will appear in the list as false, if the checkbox is checked. If the checkbox is unchecked, the attribute simply will be absent in the POST.
try: ftitle = f['title']
except: ftitle = n['title']
try: ftags = f['tags'].split(','),
except: ftags = n['tags']
if tuple == type(ftags): ftags=ftags[0]
new_ftags = []
for i in ftags:
if str(i) != "":
new_ftags.append(i)
ftags=new_ftags
try: fnotes = f['notes']
except: fnotes = n['notes']
try: fts = f['timestamp']
except: fts = n['timestamp']
try:
forder_id = f['order_id']
print(f"DEBUG(bws) edit: forder_id=f['order_id']={forder_id}")
except:
forder_id = n['order_id']
print(f"DEBUG(bws) edit: forder_id=n['order_id']={forder_id}")
faoint = False
if 'always_open_in_new_tab' in f:
print(f"DEBUG(bws): always_open_in_new_tab was in the form, so setting it to True")
faoint = True
print(f"DEBUG(bws): ftags is {ftags}")
if "new" == bid:
if "" == fts: fts = n['timestamp']
print(f"DEBUG(bws): making new bookmark")
b = bws_db.Bookmark(
source = app.config['database'],
url = f['url'],
title = ftitle,
uid = loggedin_uid,
tags = ftags,
notes = fnotes,
iid = 0,
timestamp = fts,
order_id = forder_id,
always_open_in_new_tab = faoint
)
if -1 == b.bid or "-1" == b.bid:
return "Something failed on server side.",500
else:
#return f"The server returned bid {b.bid}",201
return f"Added bookmark id {b.bid}!
Returning momentarily...",201
else:
if allowed:
# POST, allowed, but not new, so just update.
message = ""
print(f"DEBUG(bws) edit: updating bookmark {b.bid}")
if f['url'] != b.url:
b.url = f['url']
message += "
url"
if ftitle != b.title:
b.title = ftitle
message += "title"
if str(ftags) != str(b.tags):
b.tags = ftags
message += "tags"
if fnotes != b.notes:
b.notes = fnotes
message += "notes"
if str(fts) != str(b.timestamp):
b.timestamp = fts
message += "timestamp"
if str(forder_id) != str(b.order_id):
b.order_id = forder_id
message += "order_id"
if str(faoint) != str(b.always_open_in_new_tab):
b.always_open_in_new_tab = faoint
message += "always_open_in_new_tab"
if "" != message:
b.update()
message = "Bookmark attributes updated:"
message += f"Returning momentarily..."
return message,200
else:
# no changes
#message += "No changes"
message += f"Returning momentarily..."
return message,200
else:
# POST, not allowed
return f"Bad request.Returning momentarily...",401
return f"Bad request.
Returning momentarily...",400
# because html forms do not support http DELETE
@app.route("/delete/",defaults={'bid':-1},methods=['POST'])
@app.route("/delete//",methods=['GET','POST'])
@requires_session
@requires_bid_access
def delete(user=None,groups=None,allowed=False,bid="",loggedin_uid=-1,busername="",b=None):
print(f"DEBUG(bws) delete: got request to delete {bid} from loggedin_uid {user}, with allowed={allowed}")
if allowed:
text = str(b)
response = b.delete()
if 1 == response:
return f"Deleted {escape(text)}.Returning momentarily...",200
else:
return f"Got some other response, {response}.",500
else:
return f"Bad request.
Returning momentarily...",401
#############################
# xhr requests
@app.route("/view/list//")
@requires_session
def view_list_user(user=None,groups=None,username=""):
if "users" == username:
if 'Accept' in request.headers and 'application/json' not in request.headers['Accept']:
return "Only json is implemented for /view/list/users/",400
user_list = bws_db.get_all_users(app.config['database'])
print(f"DEBUG(bws) view_list_user: sending json user_list")
return jsonify(json.loads(json.dumps(user_list,cls=bws_db.BookmarkEncoder)))
else:
print(f"Request, user {user} in groups {groups}")
if user == username:
#print(f"User requesting his own bookmarks")
pass
elif app.config['ADMIN_GROUP'] in groups:
#print(f"Admin user requesting bookmarks for user {username}")
pass
else:
return Response(f"Wrong user",401)
a = bws_db.get_all_bookmarks(app.config['database'],username)
if 'Accept' in request.headers and 'application/json' in request.headers['Accept']:
print(f"DEBUG(bws): Returning user {username} bookmarks as json")
# I do not know how to jsonify with cls=bws_db.BookmarkEncoder, so I just used
# this back-and-forth thing
return jsonify(
json.loads(json.dumps(a,cls=bws_db.BookmarkEncoder)),
edit_url = url_for('edit')
)
else:
print(f"DEBUG(bws) view_list_user: Returning user {username} bookmarks as html")
return render_template("view_list.html",
bm_list = a
)
@app.route("/view/set/- //", methods=['POST'])
@requires_session
def set_computername(user=None,groups=None,item="",value=""):
print(f"DEBUG(bws): Setting session {session} item {item} to value \"{value}\".")
if item in ['computername','title']:
session[item] = value
return "OK",200
else:
return "bad item",400
## LOGIN FUNCTIONS
@app.route("/login/new")
@app.route("/login/new/")
def login_new():
"""Force the user to authenticate again. This is not about creating a new user."""
return redirect(url_for("login", new=""))
@app.route("/login/", methods=['POST','GET'])
def login(user="None"):
if request.method == "GET":
if 'user' in session and request.cookies.get('user') == session['user'] and (not 'new' in request.args):
return redirect(url_for("view"))
auth_header = request.headers.get("Authorization")
if auth_header:
print(f"DEBUG(bws): auth_header provided, but kerberos was removed... WHAT DO I DO NOW?!")
#if "negotiate" in auth_header:
# # assume we are already trying to log in with kerberos
# return redirect(url_for("login_kerberos"))
# default, show login form
return redirect(url_for("login_form"))
elif request.method == "POST":
if request.authorization:
return redirect(url_for("login_basic"),code=307)
return handle_login_ldap_from_non_ldap(request)
def get_next_ldap_server(app):
# on first ldap_login attempt, cache this lookup result:
if 'LDAP_HOSTS' not in app.config:
this_domain = urlparse(app.config['LDAP_URI']).hostname
app.config['LDAP_HOSTS'] = session_ldap.list_ldap_servers_for_domain(this_domain)
else:
# rotate them! So every ldap_login attempt will use the next ldap server in the list.
this_list = app.config['LDAP_HOSTS']
a = this_list[0]
this_list.append(a)
this_list.pop(0)
app.config['LDAP_HOSTS'] = this_list
# construct a new, full uri.
this_netloc = app.config['LDAP_HOSTS'][0]
up = urlparse(app.config['LDAP_URI'])
if up.port:
this_netloc += f":{up.port}"
this_uri = up._replace(netloc=this_netloc).geturl()
return this_uri
def ldap_login(username,password):
#print(f"DEBUG(bws): Trying user {username} with pw '{password}'")
this_uri = get_next_ldap_server(app)
# Perform the ldap interactions
user = session_ldap.authenticated_user(
server_uri=this_uri,
user_dn=username,
password=password
)
if user:
return user
else:
return False
return False
@app.route("/login/ldap", methods=['POST','GET'])
@app.route("/login/ldap/", methods=['POST','GET'])
@requires_authn_ldap
def login_ldap(user,groups=[]):
resp = Response(f'success with ldap')
resp.set_cookie('type',"ldap")
resp = login_generic(session,resp,user,groups)
return resp
def login_generic(session,resp,user,groups=[]):
resp.set_cookie('user',user)
end_time = datetime.datetime.now(datetime.timezone.utc) + app.permanent_session_lifetime
end_time_str = datetime.datetime.strftime(end_time,"%FT%TZ")
resp.set_cookie('timestamp',end_time_str)
session.permanent = True
session['user']=user
session['end_time'] = end_time_str
session['groups'] = groups
session.modified = True
return resp
@app.route("/login/form", methods=['POST','GET'])
@app.route("/login/form/", methods=['POST','GET'])
def login_form():
if request.method == "GET":
options = {
"ldap": "ldap",
"other": "other"
}
return render_template("login_form.html",
login_url = url_for("login")
)
#options=options
else:
# assume it is a POST
return redirect(url_for("login_ldap"), code=307)
def handle_login_ldap_from_non_ldap(request):
# set default logintype for user
logintype = "ldap"
# redirect to whichever option was chosen in the drop-down
if 'logintype' in request.form:
logintype = request.form['logintype']
if "ldap" == logintype:
return redirect(url_for("login_ldap"), code=307)
else:
return f"Authentication method {logintype} not supported yet.",400
@app.route("/logout")
@app.route("/logout/")
def logout():
resp = Response(f'logged out')
# not documented but is found on the Internet in a few random places:
session.clear()
resp.set_cookie('user','',expires=0)
resp.set_cookie('type','',expires=0)
resp.set_cookie('session','',expires=0)
resp.set_cookie('timestamp','',expires=0)
return resp
@app.route("/login/basic",methods=['POST','GET'])
@app.route("/login/basic/",methods=['POST','GET'])
def login_basic():
if not request.authorization:
return Response(f"Please provide username and password.",401,{'WWW-Authenticate': 'Basic'})
if 'username' not in request.authorization:
return Response(f"No username provided.",401)
if 'password' not in request.authorization:
return Response(f"No password provided.",401)
username = request.authorization.username
pw = request.authorization.password
form={'username':username,'password':pw}
session['formdata'] = form
session.modified = True
return redirect(url_for("login_ldap"),code=307)
@app.route("/settings/", methods=['GET','POST'])
@requires_session
def settings(user,groups):
print(f"DEBUG(bws): visit settings page as user {user}")
print(f"DEBUG(bws): with groups {groups}")
#if "admins" not in groups:
#return Response(f'
Not Found
What you were looking for is just not there.
Start over', 404)
#return Response(f'
Not Found
The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
', 404)
# return Response(f'Not Authorized
You are not authorized to access this page.
', 403)
if False:
a = False
else:
if request.method == "GET":
s_cn = ""
if 'computername' in session:
s_cn = session['computername']
s_t = "Bookmark Web Service"
if 'title' in session:
s_t = session['title']
return render_template(
'settings.html',
ldap_uri=app.config['LDAP_URI'],
session_computername = s_cn,
session_title = s_t
)
elif request.method == "POST":
form = request.form
print(f"Form: {form}")
message = ""
if 'computername' not in form and 'title' not in form:
return Response("Invalid input.", 400)
else:
s_cn = ""
if 'computername' in session:
s_cn = session['computername']
s_t = ""
if 'title' in session:
s_t = session['title']
if 'computername' in form and form['computername'] != s_cn:
session['computername'] = form['computername']
message += "computer name"
session.modified = True
if 'title' in form and form['title'] != s_t:
session['title'] = form['title']
message += "Tab title"
session.modified = True
if "" != message:
message = "Settings updated:"
#message += f""
pp = url_for("view")
message += f"Returning momentarily..."
message += f"""
"""
return Response(message, 200)
## This bumps the session lifetime to two minutes farther out from each web request with this session.
#@app.before_request
#def make_session_permanent():
# session.permanent = True
# session['end_time'] = datetime.datetime.now()+app.permanent_session_lifetime
if __name__ == '__main__':
print("should listen to ",app.config['LISTEN_HOST'])
app.run(
host=app.config['LISTEN_HOST'],
port=app.config['LISTEN_PORT'],
debug=app.config['DEBUG']
)