bespinserver/bespin/controllers.py

1599 строки
55 KiB
Python

# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Bespin.
#
# The Initial Developer of the Original Code is
# Mozilla.
# Portions created by the Initial Developer are Copyright (C) 2009
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
#
import os
import urllib2
import httplib2
from urlparse import urlparse
import logging
from datetime import date
import socket
import urllib
from hashlib import sha256
import re
import md5
from urlrelay import URLRelay, register
from paste.auth import auth_tkt
from paste.proxy import Proxy
import simplejson
import tempfile
import static
from webob import Request, Response
from bespin.config import c
from bespin.framework import expose, BadRequest
from bespin import vcs, deploy
from bespin.database import User, get_project, log_event, GalleryPlugin
from bespin.filesystem import NotAuthorized, OverQuota, File, FileNotFound
from bespin.utils import send_email_template
from bespin import filesystem, queue, plugins
from bespin.plugins import get_user_plugin_path, get_user_plugin_info
log = logging.getLogger("bespin.controllers")
@expose(r'^/register/new/(?P<username>.*)$', 'POST', auth=False)
def new_user(request, response):
try:
username = request.kwargs['username']
email = request.POST['email']
password = request.POST['password']
except KeyError:
raise BadRequest("username, email and password are required.")
user = User.create_user(username, password, email)
settings_project = get_project(user, user, "BespinSettings", create=True)
settings_project.install_template('usertemplate')
response.content_type = "application/json"
response.body = "{}"
request.environ['paste.auth_tkt.set_user'](username)
return response()
def _get_capabilities():
return dict(
capabilities = list(c.capabilities),
javaScriptPlugins = list(c.javascript_plugins),
dojoModulePath = c.dojo_module_path
)
@expose('^/capabilities/$', 'GET')
def capabilities(request, response):
response.content_type = "application/json"
result = _get_capabilities()
response.body = simplejson.dumps(result)
return response()
@expose(r'^/register/userinfo/$', 'GET')
def get_registered(request, response):
response.content_type = "application/json"
if request.user:
quota, amount_used = request.user.quota_info()
else:
quota = None
amount_used = None
response.body=simplejson.dumps(
dict(username=request.username,
quota=quota, amountUsed=amount_used,
serverCapabilities=_get_capabilities())
)
return response()
@expose(r'^/register/login/(?P<login_username>.+)', 'POST', auth=False)
def login(request, response):
username = request.kwargs['login_username']
password = request.POST.get('password')
fli = c.login_tracker.can_log_in(username)
if not fli.can_log_in:
response.status = "401 Not Authorized"
response.body = "Locked out due to failed login attempts."
return response()
user = User.find_user(username, password)
if not user:
c.login_tracker.login_failed(fli)
response.status = "401 Not Authorized"
response.body = "Invalid login"
return response()
c.login_tracker.login_successful(fli)
request.environ['paste.auth_tkt.set_user'](username)
response.content_type = "application/json"
response.body="{}"
return response()
@expose(r'^/register/logout/$')
def logout(request, response):
request.environ['paste.auth_tkt.logout_user']()
response.status = "200 OK"
response.body = "Logged out"
return response()
def _get_password_verify_code(user):
h = sha256()
h.update(c.secret)
h.update(user.username)
h.update(user.password)
return h.hexdigest()
@expose(r'^/register/lost/$', 'POST', auth=False)
def lost(request, response):
"""Generates lost password email messages"""
email = request.POST.get('email')
username = request.POST.get('username')
if username:
user = User.find_user(username)
if not user:
raise BadRequest("Unknown user: " + username)
verify_code = _get_password_verify_code(user)
change_url = c.base_url + "?pwchange=%s;%s" % (username, verify_code)
context = dict(username=username, base_url=c.base_url,
change_url=change_url)
send_email_template(user.email, "Requested password change for " + c.base_url,
"lost_password.txt", context)
elif email:
users = User.find_by_email(email)
context = dict(email=email,
usernames=[dict(username=user.username) for user in users],
base_url=c.base_url)
send_email_template(email, "Your username for " + c.base_url,
"lost_username.txt", context)
else:
raise BadRequest("Username or email is required.")
return response()
@expose(r'^/register/password/(?P<username>.+)$', 'POST', auth=False)
def password_change(request, response):
"""Changes a user's password."""
username = request.kwargs.get('username')
user = User.find_user(username)
if not user:
raise BadRequest("Unknown user: " + username)
verify_code = _get_password_verify_code(user)
code = request.POST.get('code')
if verify_code != code:
raise BadRequest("Invalid verification code for password change.")
user.password = User.generate_password(request.POST['newPassword'])
return response()
@expose(r'^/register/userdata/(?P<username>.+)$')
def public_user_data(request, response):
"""Request public user data."""
username = request.kwargs.get('username')
user = User.find_user(username)
if not user:
raise BadRequest("Unknown user: " + username)
response.content_type='application/json'
response.body = simplejson.dumps({
'email_md5': user.email and md5.new(user.email).hexdigest()
})
return response()
@expose(r'^/settings/$', 'POST')
def save_settings(request, response):
"""Saves one or more settings for the currently logged in user."""
user = request.user
user.settings.update(request.POST)
# make it so that the user obj appears dirty to SQLAlchemy
user.settings = user.settings
return response()
@expose(r'^/settings/(?P<setting_name>.*)$', 'GET')
def get_settings(request, response):
"""Retrieves one setting or all (depending on URL)."""
kwargs = request.kwargs
user = request.user
response.content_type='application/json'
setting_name = kwargs['setting_name']
if setting_name:
try:
response.body=simplejson.dumps(user.settings[setting_name])
except KeyError:
response.status = '404 Not Found'
response.body = '%s not found' % setting_name
response.content_type="text/plain"
else:
response.body=simplejson.dumps(user.settings)
return response()
@expose(r'^/settings/(?P<setting_name>.+)$', 'DELETE')
def delete_setting(request, response):
user = request.user
kwargs = request.kwargs
setting_name = kwargs['setting_name']
try:
del user.settings[setting_name]
# get the user to appear dirty
user.settings = user.settings
except KeyError:
response.status = "404 Not Found"
return response()
def _split_path(request):
path = request.kwargs['path']
result = path.split('/', 1)
if len(result) < 2:
raise BadRequest("Project and path are both required.")
parts = result[0].partition('+')
if parts[1] == '':
result.insert(0, request.user)
else:
result.insert(0, User.find_user(parts[0]))
result[1] = parts[2]
return result
@expose(r'^/file/listopen/$', 'GET')
def listopen(request, response):
user = request.user
result = user.files
response.content_type = "application/json"
response.body = simplejson.dumps(result)
return response()
@expose(r'^/file/at/(?P<path>.*)$', 'PUT')
def putfile(request, response):
user = request.user
owner, project, path = _split_path(request)
if (path == '' or path.endswith('/')) and request.body:
raise BadRequest("Path ended in '/' indicating directory, but request contains body text")
project = get_project(user, owner, project, create=True)
if path.endswith('/'):
if request.body != None and request.body != '':
raise BadRequest("Path ended in '/' indicating directory, but request contains ")
project.create_directory(path)
elif path:
project.save_file(path, request.body)
log_event("filesave", request.user)
return response()
@expose(r'^/file/at/(?P<path>.*)$', 'GET')
def getfile(request, response):
user = request.user
owner, project, path = _split_path(request)
project = get_project(user, owner, project)
mode = request.GET.get('mode', 'rw')
contents = project.get_file(path, mode)
_tell_file_event(user, project, path, 'open')
response.body = contents
response.content_type = "zombie/brains"
return response()
@expose(r'^/file/close/(?P<path>.*)$', 'POST')
def postfile(request, response):
user = request.user
owner, project, path = _split_path(request)
project = get_project(user, owner, project)
project.close(path)
return response()
@expose(r'^/file/at/(?P<path>.*)$', 'DELETE')
def deletefile(request, response):
user = request.user
owner, project, path = _split_path(request)
project = get_project(user, owner, project)
project.delete(path)
return response()
@expose(r'^/file/list/(?P<path>.*)$', 'GET')
def file_list(request, response):
user = request.user
path = request.kwargs['path']
files = []
if not path:
projects = request.user.get_all_projects(True)
for project in projects:
if project.owner == user:
files.append({ 'name':project.short_name })
else:
files.append({ 'name':project.owner.username + "+" + project.short_name })
else:
try:
owner, project, path = _split_path(request)
except BadRequest:
project = path
path = ''
if project:
project = get_project(user, owner, project)
project_files = project.list_files(path)
for item in project_files:
reply = { 'name':item.short_name }
_populate_stats(item, reply)
files.append(reply)
return _respond_json(response, files)
@expose(r'^/file/list_all/(?P<path>.*)$', 'GET')
def file_list_all(request, response):
user = request.user
path = request.kwargs['path']
files = []
if not path:
projects = request.user.get_all_projects(True)
for project in projects:
if project.owner == user:
pname = project.short_name
else:
pname = project.owner.username + "+" + project.short_name
metadata = project.metadata
files.extend(pname + name
for name in metadata.get_file_list())
metadata.close()
else:
owner, project_name, path = _split_path(request)
project = get_project(user, user, project_name)
metadata = project.metadata
files.extend(name for name in metadata.get_file_list(path))
metadata.close()
return _respond_json(response, files)
@expose(r'^/project/template/(?P<project_name>.*)/$', 'POST')
def install_template(request, response):
user = request.user
project_name = request.kwargs['project_name']
data = simplejson.loads(request.body)
try:
template_name = data['templateName']
except KeyError:
raise BadRequest("templateName not provided in request")
project = get_project(user, user, project_name, create=True)
project.install_template(template_name, data)
response.content_type = "text/plain"
response.body = ""
return response()
@expose(r'^/project/rescan/(?P<project_name>.*$)', 'POST')
def rescan_project(request, response):
user = request.user
project_name = request.kwargs['project_name']
project = get_project(user, user, project_name)
job_body = dict(user=user.username, project=project_name)
jobid = queue.enqueue("vcs", job_body, execute="bespin.filesystem:rescan_project",
error_handler="bespin.vcs:vcs_error",
use_db=True)
response.content_type = "application/json"
response.body = simplejson.dumps(dict(jobid=jobid,
taskname="Rescan %s" % project_name))
return response()
@expose(r'^/file/template/(?P<path>.*)', 'PUT')
def install_file_template(request, response):
user = request.user
owner, project, path = _split_path(request)
project = get_project(user, user, project, create=True)
options = simplejson.loads(request.body)
project.install_template_file(path, options)
response.body = ""
response.content_type = "text/plain"
return response()
@expose(r'^/file/search/(?P<project_name>.*)$', 'GET')
def file_search(request, response):
user = request.user
query = request.GET.get("q", "")
query = query.decode("utf-8")
include = request.GET.get("i", "")
limit = request.GET.get("limit", 20)
try:
limit = int(limit)
except ValueError:
limit = 20
project_name = request.kwargs['project_name']
project = get_project(user, user, project_name)
result = project.search_files(query, limit, include)
return _respond_json(response, result)
def _populate_stats(item, result):
if isinstance(item, File):
result['size'] = item.saved_size
result['created'] = item.created.strftime("%Y%m%dT%H%M%S")
result['modified'] = item.modified.strftime("%Y%m%dT%H%M%S")
result['openedBy'] = [username for username in item.users]
@expose(r'^/file/stats/(?P<path>.+)$', 'GET')
def filestats(request, response):
user = request.user
owner, project, path = _split_path(request)
project = get_project(user, owner, project)
file_obj = project.get_file_object(path)
result = {}
_populate_stats(file_obj, result)
return _respond_json(response, result)
@expose(r'^/project/import/(?P<project_name>[^/]+)', "POST")
def import_project(request, response):
project_name = request.kwargs['project_name']
input_file = request.POST['filedata']
filename = input_file.filename
_perform_import(request.user, project_name, filename,
input_file.file)
return response()
def _perform_import(user, project_name, filename, fileobj):
project = get_project(user, user, project_name, clean=True)
if filename.endswith(".tgz") or filename.endswith(".tar.gz"):
func = project.import_tarball
elif filename.endswith(".zip"):
func = project.import_zipfile
else:
raise BadRequest(
"Import only supports .tar.gz, .tgz and .zip at this time.")
func(filename, fileobj)
return
def validate_url(url):
if not url.startswith("http://") and not url.startswith("https://"):
raise BadRequest("Invalid url: " + url)
return url
def _download_data(url, request):
"""downloads the data to a temporary file, raising BadRequest
if there are issues, doing a HEAD request first to ensure that
the URL is good and also ensuring that the user has enough
space available."""
url = validate_url(url)
try:
resp = httplib2.Http().request(url, method="HEAD")
except httplib2.HttpLib2Error, e:
raise BadRequest(str(e))
# check the content length to see if the user has enough quota
# available before we download the whole file
content_length = resp[0].get("content-length")
if content_length:
content_length = int(content_length)
if not request.user.check_save(content_length):
raise OverQuota()
try:
datafile = urllib2.urlopen(url)
except urllib2.URLError, e:
raise BadRequest(str(e))
tempdatafile = tempfile.NamedTemporaryFile()
tempdatafile.write(datafile.read())
datafile.close()
tempdatafile.seek(0)
return tempdatafile
@expose(r'^/project/fromurl/(?P<project_name>[^/]+)', "POST")
def import_from_url(request, response):
project_name = request.kwargs['project_name']
tempdatafile = _download_data(request.body, request)
url_parts = urlparse(url)
filename = os.path.basename(url_parts[2])
_perform_import(request.user, project_name, filename, tempdatafile)
tempdatafile.close()
return response()
@expose(r'^/project/export/(?P<project_name>.*(\.zip|\.tgz))')
def export_project(request, response):
user = request.user
project_name = request.kwargs['project_name']
project_name, extension = os.path.splitext(project_name)
project = get_project(user, user, project_name)
if extension == ".zip":
func = project.export_zipfile
response.content_type = "application/zip"
else:
response.content_type = "application/x-tar-gz"
func = project.export_tarball
output = func()
def filegen():
data = output.read(8192)
while data:
yield data
data = output.read(8192)
raise StopIteration
response.app_iter = filegen()
return response()
@expose(r'^/preview/at/(?P<path>.+)$')
def preview_file(request, response):
user = request.user
owner, project, path = _split_path(request)
if owner != user:
raise BadRequest("Preview of shared projects is not currently supported for security reasons.")
project = get_project(user, owner, project)
file_obj = project.get_file_object(path)
response.body = str(file_obj.data)
response.content_type = file_obj.mimetype
return response()
@expose(r'^/project/rename/(?P<project_name>.+)/$', 'POST')
def rename_project(request, response):
user = request.user
project_name = request.kwargs['project_name']
project = get_project(user, user, project_name)
project.rename(request.body)
response.body = ""
response.content_type = "text/plain"
return response()
@expose(r'^/network/followers/$', 'GET')
def follow(request, response):
return _users_followed_response(request.user, response)
@expose(r'^/network/follow/', 'POST')
def follow(request, response):
users = _lookup_usernames(simplejson.loads(request.body))
for other_user in users:
request.user.follow(other_user)
return _users_followed_response(request.user, response)
@expose(r'^/network/unfollow/', 'POST')
def unfollow(request, response):
users = _lookup_usernames(simplejson.loads(request.body))
for other_user in users:
request.user.unfollow(other_user)
return _users_followed_response(request.user, response)
@expose(r'^/network/followers/tell/$', 'POST')
def tell(request, response):
user = request.user
post = simplejson.loads(request.body)
text = post.get('text', '*unspecified*')
username = post.get('username', '')
target_id = username and 'tell' or 'broadcast'
connections = user.users_following_me()
list = []
for connection in connections:
if not username or connection.following.username == username:
connection.following.publish({
'msgtargetid': target_id,
'from': user.username,
'text': text
})
list.append(connection.following.username)
if username:
break;
response.body = simplejson.dumps(list)
response.content_type = "text/plain"
return response()
@expose(r'^/group/list/all', 'GET')
def group_list_all(request, response):
groups = request.user.get_groups()
groups = [ group.name for group in groups ]
return _respond_json(response, groups)
@expose(r'^/group/list/(?P<group>[^/]+)/$', 'GET')
def group_list(request, response):
group_name = request.kwargs['group']
group = request.user.get_group(group_name, raise_on_not_found=True)
members = group.get_members()
members = [ member.user.username for member in members ]
return _respond_json(response, members)
@expose(r'^/group/remove/all/(?P<group>[^/]+)/$', 'POST')
def group_remove_all(request, response):
group_name = request.kwargs['group']
group = request.user.get_group(group_name, raise_on_not_found=True)
rows = 0
rows += group.remove_all_members()
rows += group.remove()
return _respond_json(response, rows)
@expose(r'^/group/remove/(?P<group>[^/]+)/$', 'POST')
def group_remove(request, response):
group_name = request.kwargs['group']
group = request.user.get_group(group_name, raise_on_not_found=True)
users = _lookup_usernames(simplejson.loads(request.body))
rows = 0
for other_user in users:
rows += group.remove_member(other_user)
members = group.get_members()
if len(members) == 0:
rows += group.remove()
return _respond_json(response, rows)
@expose(r'^/group/add/(?P<group>[^/]+)/$', 'POST')
def group_add(request, response):
group_name = request.kwargs['group']
group = request.user.get_group(group_name, create_on_not_found=True)
users = _lookup_usernames(simplejson.loads(request.body))
for other_user in users:
group.add_member(other_user)
return _respond_blank(response)
def _respond_blank(response):
response.body = ""
response.content_type = "text/plain"
return response()
def _respond_json(response, data):
response.body = simplejson.dumps(data)
response.content_type = "application/json"
return response()
def _lookup_usernames(usernames):
def lookup_username(username):
user = User.find_user(username)
if user == None:
raise BadRequest("Username not found: %s" % username)
return user
return map(lookup_username, usernames)
def _users_followed_response(user, response):
list = user.users_i_follow()
list = [connection.followed.username for connection in list]
response.body = simplejson.dumps(list)
response.content_type = "text/plain"
return response()
def _users_following_response(user, response):
list = user.users_following_me()
list = [connection.following.username for connection in list]
response.body = simplejson.dumps(list)
response.content_type = "text/plain"
return response()
def _tell_file_event(user, project, path, event):
followers = user.users_following_me()
followers = [follower.following.username for follower in followers if follower.following]
# find the owner
isMyProject = _is_project_shared(project, user)
for follower in followers:
#try:
member = User.find_user(follower)
if isMyProject and _is_project_shared(project, member):
# we can safely send a message
member.publish({
'msgtargetid': 'file_event',
'from': user.username,
'event': event,
'project': project.name,
'owner': project.owner.username,
'path': path,
})
#except:
#pass
def _is_project_shared(project, user):
return project.owner.username == user.username or project.owner.is_project_shared(project, user)
@expose(r'^/share/list/all/$', 'GET')
def share_list_all(request, response):
"List all project shares"
data = request.user.get_sharing()
return _respond_json(response, data)
@expose(r'^/share/list/(?P<project>[^/]+)/$', 'GET')
def share_list_project(request, response):
"List sharing for a given project"
project = get_project(request.user, request.user, request.kwargs['project'])
data = request.user.get_sharing(project)
return _respond_json(response, data)
@expose(r'^/share/list/(?P<project>[^/]+)/(?P<member>[^/]+)/$', 'GET')
def share_list_project_member(request, response):
"List sharing for a given project and member"
project = get_project(request.user, request.user, request.kwargs['project'])
member = request.user.find_member(request.kwargs['member'])
data = request.user.get_sharing(project, member)
return _respond_json(response, data)
@expose(r'^/share/remove/(?P<project>[^/]+)/all/$', 'POST')
def share_remove_all(request, response):
"Remove all sharing from a project"
project = get_project(request.user, request.user, request.kwargs['project'])
data = request.user.remove_sharing(project)
return _respond_json(response, data)
@expose(r'^/share/remove/(?P<project>[^/]+)/(?P<member>[^/]+)/$', 'POST')
def share_remove(request, response):
"Remove project sharing from a given member"
project = get_project(request.user, request.user, request.kwargs['project'])
member = request.user.find_member(request.kwargs['member'])
data = request.user.remove_sharing(project, member)
return _respond_json(response, data)
@expose(r'^/share/add/(?P<project>[^/]+)/(?P<member>[^/]+)/$', 'POST')
def share_add(request, response):
"Add a member to the sharing list for a project"
project = get_project(request.user, request.user, request.kwargs['project'])
member = request.user.find_member(request.kwargs['member'])
options = simplejson.loads(request.body)
request.user.add_sharing(project, member, options)
return _respond_blank(response)
@expose(r'^/share/tell/(?P<project>[^/]+)/$', 'POST')
def share_tell(request, response):
user = request.user
project_name = request.kwargs['project']
post = simplejson.loads(request.body)
text = post.get('text', '*unspecified*')
# find the owner
parts = project_name.partition('+')
if parts[1]:
owner = User.find_user(parts[0])
project_name = parts[2]
else:
owner = user
project = get_project(user, owner, project_name)
isMyProject = _is_project_shared(project, user)
# notify recipients
list = []
recipients = post.get('recipients', [])
for recipient in recipients:
#try:
member = User.find_user(recipient)
if isMyProject and _is_project_shared(project, member):
# we can safely send a message
member.publish({
'msgtargetid': 'share_tell',
'from': user.username,
'text': text
})
list.append(recipient)
#except:
# pass
response.body = simplejson.dumps(list)
response.content_type = "text/plain"
return response()
@expose(r'^/viewme/list/all/$', 'GET')
def viewme_list_all(request, response):
"List all the members with view settings on me"
data = request.user.get_viewme()
return _respond_json(response, data)
@expose(r'^/viewme/list/(?P<member>[^/]+)/$', 'GET')
def viewme_list(request, response):
"List the view settings for a given member"
member = request.user.find_member(request.kwargs['member'])
data = request.user.get_viewme(member)
return _respond_json(response, data)
@expose(r'^/viewme/set/(?P<member>[^/]+)/(?P<value>[^/]+)/$', 'POST')
def viewme_set(request, response):
"Alter the view setting for a given member"
member = request.user.find_member(request.kwargs['member'])
value = request.kwargs['value']
data = request.user.set_viewme(member, value)
return _respond_json(response, data)
from bespin.mobwrite.mobwrite_daemon import DaemonMobWrite
from bespin.mobwrite.mobwrite_daemon import maybe_cleanup
class MobwriteInProcess(DaemonMobWrite):
"Talk to an in-process mobwrite"
def processRequest(self, question):
"Since we are a MobWriteWorker we just call directly into mobwrite code"
answer = self.handleRequest(question)
maybe_cleanup()
return answer
class MobwriteTelnetProxy():
"Talk to mobwrite using port 3017"
def processRequest(self, question):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((c.mobwrite_server_address, c.mobwrite_server_port))
s.send(question)
answer = ''
while True:
line = s.recv(1024)
if not line:
break
answer += line
s.close()
return answer
class MobwriteHttpProxy():
"Talk to mobwrite over HTTP"
def processRequest(self, question):
url = "http://%s:%s/" % (c.mobwrite_server_address,
c.mobwrite_server_port)
try:
datafile = urllib2.urlopen(url, question)
except urllib2.URLError, e:
raise BadRequest(str(e))
answer = datafile.read()
datafile.close()
return answer
@expose(r'^/mobwrite/$', 'POST')
def mobwrite(request, response):
"""Handle a request for mobwrite synchronization.
We talk to mobwrite either in-process for development or using a socket
which would be more common in live."""
c.stats.incr("mobwrite_DATE")
question = urllib.unquote(request.body)
# Hmmm do we need to handle 'p' requests? q.py does.
mode = None
if question.find("p=") == 0:
mode = "script"
elif question.find("q=") == 0:
mode = "text"
else:
raise BadRequest("Missing q= or p=")
question = question[2:]
question = "H:" + str(request.user.username) + "\n" + question
# Java: Class.forName(...) There *has* to be a better way in python?
if c.mobwrite_implementation == "MobwriteInProcess":
worker = MobwriteInProcess()
if c.mobwrite_implementation == "MobwriteTelnetProxy":
worker = MobwriteTelnetProxy()
if c.mobwrite_implementation == "MobwriteHttpProxy":
worker = MobwriteHttpProxy()
#log.debug("\n\nQUESTION:\n" + question);
answer = worker.processRequest(question)
#log.debug("\nANSWER:\n" + answer + "\n");
if mode == "text":
response.body = answer + "\n\n"
response.content_type = "text/plain"
else:
answer = answer.replace("\\", "\\\\").replace("\"", "\\\"")
answer = answer.replace("\n", "\\n").replace("\r", "\\r")
answer = "mobwrite.callback(\"%s\");" % answer
response.body = answer
response.content_type = "application/javascript"
return response()
test_users = [ "ev", "tom", "mattb", "zuck" ]
@expose(r'^/test/setup/$', 'POST')
def test_setup(request, response):
for name in test_users:
user = User.find_user(name)
if (user == None):
user = User.create_user(name, name, name)
response.body = ""
response.content_type = "text/plain"
return response()
@expose(r'^/test/cleanup/$', 'POST')
def test_cleanup(request, response):
response.body = ""
response.content_type = "text/plain"
return response()
@expose(r'^/vcs/clone/$', 'POST')
def vcs_clone(request, response):
user = request.user
source = request.POST.get("source")
taskname = "Clone/checkout"
if source:
taskname += " from %s" % (source)
jobid = vcs.clone(user, **dict(request.POST))
response.content_type = "application/json"
response.body = simplejson.dumps(dict(jobid=jobid, taskname=taskname))
return response()
@expose(r'^/vcs/command/(?P<project_name>.*)/$', 'POST')
def vcs_command(request, response):
user = request.user
project_name = request.kwargs['project_name']
request_info = simplejson.loads(request.body)
args = request_info['command']
log.debug("VCS command: %s", args)
kcpass = request_info.get('kcpass')
try:
taskname = "vcs %s command" % (args[0])
except IndexError:
taskname = "vcs command"
# special support for clone/checkout
if vcs.is_new_project_command(args):
raise BadRequest("Use /vcs/clone/ to create a new project")
else:
project = get_project(user, user, project_name)
jobid = vcs.run_command(user, project, args, kcpass)
response.content_type = "application/json"
response.body = simplejson.dumps(dict(jobid=jobid, taskname=taskname))
return response()
@expose(r'^/vcs/remoteauth/(?P<project_name>.*)/$', 'GET')
def vcs_remoteauth(request, response):
user = request.user
project_name = request.kwargs['project_name']
project = get_project(user, user, project_name)
metadata = project.metadata
value = metadata.get(vcs.AUTH_PROPERTY, "")
response.content_type = "text/plain"
response.body = value.encode("utf8")
return response()
@expose(r'^/vcs/setauth/(?P<project_name>.*)/$', 'POST')
def keychain_setauth(request, response):
user = request.user
project_name = request.kwargs['project_name']
project = get_project(user, user, project_name)
try:
kcpass = request.POST['kcpass']
atype = request.POST['type']
remote_auth = request.POST['remoteauth']
except KeyError:
raise BadRequest("Request must include kcpass, type and remoteauth.")
if remote_auth != vcs.AUTH_WRITE and remote_auth != vcs.AUTH_BOTH:
raise BadRequest("Remote auth type must be %s or %s" %
(vcs.AUTH_WRITE, vcs.AUTH_BOTH))
keychain = vcs.KeyChain(user, kcpass)
body = ""
if atype == "password":
try:
username = request.POST['username']
password = request.POST['password']
except KeyError:
raise BadRequest("Request must include username and password")
keychain.set_credentials_for_project(project, remote_auth, username,
password)
elif atype == "ssh":
# set the project to use the SSH key and return the public key
body = keychain.set_ssh_for_project(project, remote_auth)[0]
else:
raise BadRequest("auth type must be ssh or password")
response.content_type = "application/json"
response.body = body
return response()
@expose("^/vcs/getkey/$", 'POST')
def get_ssh_key(request, response):
user = request.user
try:
kcpass = request.POST['kcpass']
except KeyError:
kcpass = None
if kcpass is None:
pubkey = vcs.KeyChain.get_ssh_public_key(user)
else:
keychain = vcs.KeyChain(user, kcpass)
pubkey = keychain.get_ssh_key()[0]
response.content_type = "application/x-ssh-key"
response.body = pubkey
return response()
def ask_mobwrite(question, user):
"""Handle a request for mobwrite synchronization.
We talk to mobwrite either in-process for development or using a socket
which would be more common in live."""
if not question:
return ""
c.stats.incr("mobwrite_DATE")
question = "H:" + str(user.username) + "\n" + question
# Java: Class.forName(...) There *has* to be a better way in python?
if c.mobwrite_implementation == "MobwriteInProcess":
worker = MobwriteInProcess()
if c.mobwrite_implementation == "MobwriteTelnetProxy":
worker = MobwriteTelnetProxy()
if c.mobwrite_implementation == "MobwriteHttpProxy":
worker = MobwriteHttpProxy()
#log.debug("\n\nQUESTION:\n" + question);
answer = worker.processRequest(question)
#log.debug("\nANSWER:\n" + answer + "\n");
return answer
@expose("^/messages/$", 'POST')
def messages(request, response):
c.stats.incr("messages_DATE")
user = request.user
if user is None:
body = u"[]"
else:
question = request.body
msgs = [simplejson.loads(msg) for msg in user.pop_messages()]
if "collab" in c.capabilities:
answer = ask_mobwrite(question, user) + "\n\n"
msgs.append({
"msgtargetid": "mobwrite",
"from": user.username,
"text": answer
})
body = simplejson.dumps(msgs)
response.content_type = "application/json"
response.body = body.encode("utf8")
return response()
@expose('^/stats/$', 'GET')
def stats(request, response):
username = request.username
if username not in c.stats_users:
raise NotAuthorized("Not allowed to access stats")
today = date.today().strftime("%Y%m%d")
keys = ["exceptions_" + today,
'requests_' + today,
'mobwrite_' + today,
'messages_' + today,
'users',
'files',
'projects',
'vcs_' + today]
more_keys = [k.replace("_DATE", "_" + today) for k in c.stats_display]
keys.extend(more_keys)
result = c.stats.multiget(keys)
response.content_type = "application/json"
response.body = simplejson.dumps(result)
return response()
@expose('^/project/deploy/(?P<project_name>[^/]+)/setup$', 'PUT')
def deploy_setup(request, response):
user = request.user
project_name = request.kwargs['project_name']
project = get_project(user, user, project_name)
data = simplejson.loads(request.body)
deploy_options = dict(remote_host = data.get("remoteHost"),
remote_directory = data.get("remoteDirectory"),
type = data.get("connType"))
try:
pdo = deploy.ProjectDeploymentOptions(project, **deploy_options)
pdo.save()
except deploy.InvalidConfiguration, e:
raise BadRequest(e.message)
keychain = deploy.DeploymentKeyChain(user, data['kcpass'])
if data['authType'] == "ssh":
keychain.set_ssh_for_project(project, username=data['username'])
else:
keychain.set_credentials_for_project(project,
username=data['username'],
password=data['password'])
project.metadata.close()
response.content_type="application/json"
response.body=""
return response()
@expose('^/project/deploy/(?P<project_name>[^/]+)/setup$', 'POST')
def retrieve_deploy_setup(request, response):
user = request.user
project_name = request.kwargs['project_name']
project = get_project(user, user, project_name)
data = simplejson.loads(request.body)
response.content_type="application/json"
pdo = deploy.ProjectDeploymentOptions.get(project)
if not pdo:
response.body = simplejson.dumps(None)
return response()
kc = deploy.DeploymentKeyChain(user, data['kcpass'])
cred = kc.get_credentials_for_project(project)
result = dict(remoteHost=pdo.remote_host,
remoteDirectory=pdo.remote_directory,
connType=pdo.type, authType=cred['type'],
username=cred['username'])
result['password'] = cred['password'] \
if cred['type'] == "password" else ""
response.body = simplejson.dumps(result)
return response()
@expose(r'^/project/deploy/(?P<project_name>[^/]+)/$', 'POST')
def run_deploy(request, response):
user = request.user
project_name = request.kwargs['project_name']
data = simplejson.loads(request.body)
kcpass = data['kcpass']
options = dict()
if "dryRun" in data:
options['dry_run'] = data['dryRun']
project = get_project(user, user, project_name)
response.content_type = "application/json"
try:
jobid = deploy.run_deploy(user, project, kcpass, options)
except deploy.NotConfigured, e:
response.body = simplejson.dumps(dict(error=str(e),
notConfigured=True))
response.status = "400 Bad Request"
return response()
response.body = simplejson.dumps(dict(jobid=jobid,
taskname="deploy %s" % (project_name)))
return response()
def _plugin_does_not_exist(response, plugin_name):
response.status = "404 Not Found"
response.content_type = "text/plain"
response.body = "Plugin '" + plugin_name + "' does not exist."
return response()
def _plugin_response(response, path=None, plugin_list=None, log_user=None,
environment='main', filter_plugins=True, plugin_info=None):
response.content_type = "application/json"
if plugin_list is None:
plugin_list = plugins.find_plugins(path)
def validate_env(plugin):
metadata = plugin.metadata
if 'environments' not in metadata:
return True
environments = metadata['environments']
return environment not in environments or environments[environment]
if filter_plugins:
metadata = dict((plugin.name, plugin.metadata)
for plugin in plugins.filter_plugins(plugin_list, validate_env))
else:
metadata = dict((plugin.name, plugin.metadata)
for plugin in plugin_list)
if log_user:
log_event("userplugin", log_user, len(metadata))
if plugin_info is None:
resp_body = metadata
else: # user plugins
resp_body = {
'metadata': metadata,
'ordering': plugin_info.get('ordering', []),
'deactivated': plugin_info.get('deactivated', {})
}
response.body = simplejson.dumps(resp_body)
return response()
@expose(r'^/plugin/register/defaults$', 'GET', auth=False)
def register_plugins(request, response):
return _plugin_response(response)
@expose(r'^/plugin/register/boot$', 'GET', auth=False)
def register_boot_plugin(request, response):
for item in c.plugin_path:
if item["name"] == "boot":
break
if item["name"] != "boot":
raise BadRequest("No boot code available")
plugin_list = plugins.find_plugins([item])
output = ""
for plugin in plugin_list:
output += """
%s.register('::%s', %s);
""" % (c.loader_name, plugin.name, simplejson.dumps(plugin.metadata))
response.content_type = "text/javascript"
response.body = output
return response()
def _user_plugin_response(request, response, environment='main'):
plugin_info, project = get_user_plugin_info(request.user)
path = get_user_plugin_path(request.user, plugin_info=plugin_info,
project=project)
return _plugin_response(response, path, environment=environment,
plugin_info=plugin_info)
@expose(r'^/plugin/register/user$', 'GET', auth=True)
def register_user_plugins(request, response):
return _user_plugin_response(request, response)
@expose(r'^/plugin/register/user-worker$', 'GET', auth=True)
def register_user_worker_plugins(request, response):
return _user_plugin_response(request, response, 'worker')
@expose(r'^/plugin/register/tests$', 'GET', auth=False)
def register_test_plugins(request, response):
if "test_plugin_path" not in c:
raise FileNotFound("Test plugins are only in development environment")
return _plugin_response(response, c.test_plugin_path)
@expose(r'^/plugin/register/worker$', 'GET', auth=False)
def register_worker_plugins(request, response):
return _plugin_response(response, environment='worker')
@expose(r'^/plugin/script/(?P<plugin_location>[^/]+)/(?P<plugin_name>[^/]+)/(?P<path>.*)', 'GET', auth=False)
def load_script(request, response):
response.content_type = "text/javascript"
plugin_name = request.kwargs['plugin_name']
plugin_location = request.kwargs['plugin_location']
script_path = request.kwargs['path']
if ".." in plugin_name or ".." in script_path or ".." in plugin_location:
raise BadRequest("'..' not allowed in plugin or script names")
path = None
for path_entry in c.plugin_path:
if path_entry['name'] == plugin_location:
path = path_entry
if path is None:
raise FileNotFound("Plugin location %s unknown" % (plugin_location))
plugin = plugins.lookup_plugin(plugin_name, [path])
if not plugin:
return _plugin_does_not_exist(response, plugin_name)
script_text = plugin.get_script_text(script_path)
response.body = _wrap_script(plugin_name, script_path, script_text)
return response()
@expose(r'^/plugin/file/(?P<plugin_location>[^/]+)/(?P<plugin_name>[^/]+)/(?P<path>.*)', 'GET', auth=False)
def load_file(request, response):
plugin_name = request.kwargs['plugin_name']
plugin_location = request.kwargs['plugin_location']
script_path = request.kwargs['path']
if ".." in plugin_name or ".." in script_path or ".." in plugin_location:
raise BadRequest("'..' not allowed in plugin or script names")
path = None
for path_entry in c.plugin_path:
if path_entry['name'] == plugin_location:
path = path_entry
if path is None:
raise FileNotFound("Plugin location %s unknown" % (plugin_location))
plugin = plugins.lookup_plugin(plugin_name, [path])
if not plugin:
return _plugin_does_not_exist(response, plugin_name)
# use the static package to actually serve the file
newapp = static.Cling(plugin.location)
request.path_info = "/" + "/".join(request.path_info.split("/")[5:])
return newapp(request.environ, response.start_response)
@expose(r'^/plugin/reload/(?P<plugin_name>.+)', 'GET', auth=False)
def reload_plugin(request, response):
plugin_name = request.kwargs['plugin_name']
if ".." in plugin_name:
raise BadRequest("'..' not allowed in plugin names")
if request.user:
path = get_user_plugin_path(request.user)
else:
path = []
path.extend(c.plugin_path)
plugin = plugins.lookup_plugin(plugin_name, path)
if plugin is None:
return _plugin_does_not_exist(response, plugin_name)
return _plugin_response(response, plugin_list=[plugin],
filter_plugins=False)
@expose(r'^/plugin/upload/(?P<plugin_name>.+)', 'POST')
def upload_plugin(request, response):
plugin_name = request.kwargs['plugin_name']
if ".." in plugin_name:
raise BadRequest("'..' not allowed in plugin names")
path = get_user_plugin_path(request.user, include_installed=False)
plugin = plugins.lookup_plugin(plugin_name, path)
if not plugin:
raise plugins.PluginError("Cannot find plugin '%s' among user editable plugins" % plugin_name)
if plugin.location_name != "user":
raise BadRequest("Only user-editable plugins can be uploaded")
plugins.save_to_gallery(request.user, plugin.location)
response.content_type = "text/plain"
response.body = "OK"
return response()
@expose(r'^/plugin/gallery/$', 'GET')
def plugin_gallery(request, response):
plugins = GalleryPlugin.get_all_plugins()
result = []
for p in plugins:
data = dict(name=p.name,
description=p.package_info.get('description', ""))
result.append(data)
return _respond_json(response, result)
@expose(r'^/plugin/templates/(?P<plugin_name>.*)/$', 'GET', auth=False)
def plugin_templates(request, response):
plugin_name = request.kwargs['plugin_name']
if ".." in plugin_name:
raise BadRequest("'..' not allowed in plugin names")
if request.user:
path = get_user_plugin_path(request.user)
else:
path = []
path.extend(c.plugin_path)
plugin = plugins.lookup_plugin(plugin_name, path)
if plugin is None:
return _plugin_does_not_exist(response, plugin_name)
template_module = plugin.template_module
if not template_module:
raise FileNotFound("Plugin %s has no templates" % plugin_name)
response.content_type = "text/javascript"
response.body = _wrap_script(plugin_name, "templates", template_module)
return response()
class FileIterable(object):
def __init__(self, filename):
self.filename = filename
def __iter__(self):
return FileIterator(self.filename)
class FileIterator(object):
chunk_size = 4096
def __init__(self, filename):
self.filename = filename
self.fileobj = open(self.filename, 'rb')
def __iter__(self):
return self
def next(self):
chunk = self.fileobj.read(self.chunk_size)
if not chunk:
raise StopIteration
return chunk
@expose(r'^/plugin/download/(?P<plugin_name>[^/]+)/current/$', "GET", auth=False)
def download_plugin(request, response):
plugin_name = request.kwargs['plugin_name']
plugin = GalleryPlugin.get_plugin(plugin_name)
if plugin is None:
raise FileNotFound("Plugin %s is not in the gallery" % (plugin_name))
location = plugin.get_path()
if location.endswith(".zip"):
response.content_type = "application/zip"
else:
response.content_type = "text/javascript"
response.app_iter = FileIterable(location)
response.content_length = os.path.getsize(location)
response.last_modified = os.path.getmtime(location)
response.etag = '%s-%s-%s' % (response.last_modified,
response.content_length, hash(location))
return response()
def _wrap_script(plugin_name, script_path, script_text):
if script_path:
module_name = os.path.splitext(script_path)[0]
else:
module_name = "index"
return """; %s.module('%s:%s', function(require, exports, module) {%s
;}); %s.script('%s:%s');""" % (c.loader_name, plugin_name, module_name,
script_text, c.loader_name, plugin_name, script_path)
urlmatch = re.compile(r'^(http|https)://')
@expose(r'^/plugin/install/$', 'POST')
def install_plugin_from_url(request, response):
"""Installs a plugin into the user's BespinSettings/plugins directory."""
user = request.user
url = request.POST.get('url')
plugin_name = request.POST.get('pluginName')
if not url or not urlmatch.search(url):
raise BadRequest("URL not provided")
if plugin_name and ("/" in plugin_name or ".." in plugin_name):
raise BadRequest("Invalid plugin name. / and .. are not permitted")
tempdatafile = _download_data(url, request)
settings_project = get_project(user, user, "BespinSettings")
path_entry = dict(name="user", chop=len(user.get_location()))
plugin = plugins.install_plugin(tempdatafile, url, settings_project,
path_entry, plugin_name)
tempdatafile.close()
plugin_collection = dict()
plugin_collection[plugin.name] = plugin.metadata
response.body = simplejson.dumps(plugin_collection)
response.content_type = "application/json"
return response()
@expose(r'^/plugin/install/(?P<name>.+)$', 'POST')
def install_plugin_from_gallery(request, response):
plugin_name = request.kwargs["name"]
data = plugins.install_plugin_from_gallery(request.user, plugin_name)
return _respond_json(response, data)
def db_middleware(app):
def wrapped(environ, start_response):
from bespin import model
from sqlalchemy.orm import scoped_session
session = c.session_factory()
environ['bespin.docommit'] = True
try:
# If you need to work out what <script> tags to insert into a
# page to get Dojo to behave properly, then uncomment these 3
# path_info = environ["PATH_INFO"]
# if path_info.endswith(".js"):
# print "<script type='text/javascript' src='%s'></script>" % path_info
result = app(environ, start_response)
if result == None:
log.error("WSGI response == None")
if environ['bespin.docommit']:
session.commit()
else:
session.rollback()
except:
session.rollback()
c.stats.incr("exceptions_DATE")
log.exception("Error raised during request: %s", environ)
raise
c.stats.disconnect()
return result
return wrapped
def pathpopper_middleware(app, num_to_pop=1):
def new_app(environ, start_response):
req = Request(environ)
for i in range(0, num_to_pop):
req.path_info_pop()
return app(environ, start_response)
return new_app
_separate_plugin_name = re.compile("/([^/]+):")
def scriptwrapper_middleware(app):
def new_app(environ, start_response):
req = Request(environ)
if req.path_info.startswith("/getscript"):
if ":" not in req.path_info:
raise BadRequest(": delimiter required to separate plugin name from plugin file")
req.path_info_pop()
url_leading, plugin_name, script_path = _separate_plugin_name.split(req.path_info)
if not script_path:
req.path_info = req.path_info[:-1]
else:
req.path_info = req.path_info.replace(":", "/")
plugin_name = plugin_name.replace(".js", "")
process_script = True
else:
process_script = False
result = req.get_response(app)
if process_script and result.status.startswith("200"):
contents = result.body
newbody = _wrap_script(plugin_name, script_path, contents)
result.headers['Content-Length'] = str(len(newbody))
result.headers['Content-Type'] = "text/javascript"
start_response(result.status, result.headers.items())
return [newbody]
start_response(result.status, result.headers.items())
return [result.body]
return new_app
class URLRelayCompatibleProxy(Proxy):
"""URLRelay deep copies items from its cache, but there's
something on Paste's Proxy class that doesn't deepcopy
safely. This class works fine."""
def __deepcopy__(self, memo):
return self
def make_app():
from webob import Response
static_app = static.Cling(c.static_dir)
if c.static_override:
from paste.cascade import Cascade
static_app = Cascade([static.Cling(c.static_override), static_app])
docs_app = pathpopper_middleware(static.Cling(c.docs_dir))
code_app = pathpopper_middleware(static.Cling(c.static_dir + "/js"), 2)
register("^/docs/code/", code_app)
register("^/docs/", docs_app)
proxy_app = URLRelayCompatibleProxy("http://localhost:8081/")
register("^/.js/", proxy_app)
for location, directory in c.static_map.items():
topop = 1 + location.count('/')
more_static = pathpopper_middleware(static.Cling(directory), topop)
register("^/%s/" % location, more_static)
app = URLRelay(default=static_app)
app = auth_tkt.AuthTKTMiddleware(app, c.secret, secure=c.secure_cookie,
include_ip=False, httponly=c.http_only_cookie,
current_domain_cookie=c.current_domain_cookie, wildcard_cookie=False)
app = db_middleware(app)
if c.log_requests_to_stdout:
from paste.translogger import TransLogger
app = TransLogger(app)
app = scriptwrapper_middleware(app)
return app