def components(request, id=None, type=None): ''' Standard API endpoint for components ''' api = API(request) if api.get: filters = {} if type: type = type[:-1] # Remove the trailing 's' that is in the URL filters['type'] = type components = Component.list( user=request.user, address=api.optional('address'), published=api.optional('published', True, lambda value: bool(int(value))), filters=filters, sort=api.optional('sort', 'id') ) return api.respond( data=components, paginate=25 ) elif api.post: api.authenticated_or_raise() component = Component.create_one( user=request.user, address=api.required('address'), type=api.required('type'), ) return api.respond( data=component ) else: api.raise_method_not_allowed()
def component_one(request, id): # TODO integrate into the above api = API(request) if api.get: component = Component.read_one( id=id, user=request.user, ) return api.respond( data=component ) elif api.patch: # Currently, PATCH is not valid for components api.raise_method_not_allowed() # api.authenticated_or_raise() # component = Component.update_one( # id=id, # user=request.user, # data=api.data # ) # return api.respond( # data=component # ) elif api.delete: api.authenticated_or_raise() Component.delete_one( id=id, user=request.user ) return api.respond() else: api.raise_method_not_allowed()
def content(request, address): ''' Get or set the content of a stencil ''' api = API(request) if api.get: component = Component.get( id=None, user=request.user, action=READ, address=address ) result = component.content_get( format=api.optional('format', 'html'), ) elif api.put: component = Component.get( id=None, user=request.user, action=UPDATE, address=address ) result = component.content_set( user=request.user, format=api.required('format'), content=api.required('content'), revision=api.required('revision') ) return api.respond(result)
def test_tiny(self): id = 1000 alphanum = "QI" self.assertEqual(Component.tiny_convert(id), alphanum) self.assertEqual(Component.tiny_convert(alphanum), id) com = Component(id=id) self.assertEqual(com.tiny_url, "https://stenci.la/%s~" % alphanum)
def route(request, address=''): ''' Route a request for component/s at an address If the address matches that of a single component then open in. Otherwise show a list of all components which have the address as a prefix ''' # Remove any trailing slash for address matches if address.endswith('/'): address = address[:-1] # Check to see if there is a component matching this address # which the user is authorized to read component = Component.get( id=None, user=request.user, action=READ, address=address ) if component: api = API(request) if api.accept == 'json': return api.respond(component) else: # Yes, return the components page or # redirect to the component's canonical URL if necessary if request.path == component.url(): return page(request, address, component) else: return redirect(component.url(), permanent=True) else: # Is there an `all` stencil at the address? all = address+'/all' component = Component.get(request.user, READ, address=all) if component: # Yes, return that all stencil's page; ensure trailing slash if request.path.endswith('/'): return page(request, all, component) else: return redirect(request.path+'/') else: # Is there an `any` stencil at the parent address? parts = address.split('/') any = '/'.join(parts[:-1]) + '/any' component = Component.get(request.user, READ, address=any) if component: # Yes, return that any stencil's page; ensure trailing slash if request.path.endswith('/'): return page(request, any, component) else: return redirect(request.path+'/') else: # No, respond with stencil that provides a listing of # components at the address return page(request, '/core/stencils/index')
def tiny(request, tiny): ''' Redirect a tiny URL to the component's canonical URL ''' id = Component.tiny_convert(tiny) component = Component.get( id=id, user=request.user, action=READ, ) return redirect(component.url(), permanent=True)
def ping(request, address): ''' Ping the component's session. Returns the session id since that could change ''' api = API(request) if api.put: if not request.user.is_authenticated(): return api.respond_signin() else: component = Component.get( id=None, user=request.user, action=READ, address=address ) session = component.activate( user=request.user ) session.ping() return api.respond({ 'session': session.id }) return api.respond_bad()
def boot(request, address): ''' Boot up a component ''' api = API(request) if api.put: component = Component.get( id=None, user=request.user, action=READ, address=address ) action, grantor = component.rights(request.user) if action >= EXECUTE: api.user_ensure() session = component.activate( user=request.user ) else: session = None return api.respond({ 'rights': { 'action': action_string(action), 'grantor': grantor.serialize(user=request.user) if grantor else None }, 'session': session.serialize(user=request.user) if session else None }) return api.respond_bad()
def method(request, address, method): ''' A general view for calling methods on components that are hosted in a session. Will launch a new session for a component if necessary. Not all component methods need an active session so views should be explicitly provided for those below. ''' api = API(request) if api.put: if not request.user.is_authenticated(): return api.respond_signin() else: component = Component.get( id=None, user=request.user, action=READ, # TODO : allow for alternative rights address=address ) session = component.activate( user=request.user ) status, body = session.request( resource=address, verb=request.method, method=method, data=request.body ) return api.respond( data=body, raw=True, status=status ) return api.respond_bad()
def new(request, type): ''' Create a new component for the user and redirect them to it's canonical page Guests should not be able to login. Can't use the `user_passes_test` decorators here to make use of `?next=/new/stencil` becuase for guest that results in a redirect loop (they need to be signed out first). This should be made better sometime. ''' if request.user_agent.is_bot: return redirect('/', permanent=True) elif not request.user.is_authenticated(): return redirect('/me/signup') elif request.user.details.guest: logout(request) return redirect('/me/signup') else: type = type[:-1] # Remove the trailing 's' that is in the URL component = Component.create( user=request.user, address=None, type=type ) return redirect(component.url())
def live_html(request, address): ''' Get the HTML of a live copy of a document This should be generalised and/or merge wiith the `content` view (which also gets (and sets) HTML). ''' api = API(request) if api.get: # First, check access rights component = Component.get( id=None, address=address, user=request.user, action=READ ) # Get HTML from broker response = requests.get('http://10.0.1.75:7315/' + address + '@live?format=html') if response.status_code != 200: raise Exception(response.text) else: if api.browser: response = HttpResponse(response.json()['data'], content_type='text/html') response['Content-Disposition'] = 'attachment; filename="document.html"' return response else: return api.respond(data=response.json()) return api.respond_bad()
def live(request, address): ''' Open the live collaboration clone for the component. ''' api = API(request) if api.get: # First, check access rights component = Component.get( id=None, address=address, user=request.user, action=ANNOTATE ) payload = {} # Then, see if a live collaboration clone already exists # Get the snapshot data # TODO: In the future there may be several collaboration servers response = requests.get('http://10.0.1.75:7315/' + address + '@live') if response.status_code == 404: response = requests.post('http://10.0.1.75:7315/', json = { 'schemaName': 'stencila-document', 'documentId': address + '@live', 'format': 'html', 'content': component.content_get('html')['content'] }) if response.status_code != 200: raise Exception(response.text) payload['snapshot'] = response.json() # Insert the collaboration websocket URL if settings.MODE == 'local': # It's not possible to do a Websocket redirect (without Nginx's 'X-Accel-Redirect') so # in development just give direct WS URL of collaboration server ws = 'ws://10.0.1.75:7315' else: ws = 'wss://stenci.la' ws += '/%s@collaborate?permit=%s' % ( address, hmac.new(settings.SECRET_KEY, address + ':' + request.user.username).hexdigest() ) payload['collabUrl'] = ws # Insert the user and access rights action, grantor = component.rights(request.user) payload['user'] = request.user.username payload['rights'] = action_string(action) # Render template with snapshot data embedded in it return api.respond( template = 'components/live.html', context = { 'type': 'document', 'payload': json.dumps(payload) } ) return api.respond_bad()
def explore(request): ''' Explore components ''' components = Component.list( user=request.user, published=True, sort='-id' ) return render(request, 'explore.html', { 'components': components })
def commits(request, address): ''' Get a list of commits for a component ''' api = API(request) component = Component.get( id=None, user=request.user, action=READ, address=address ) return api.respond(component.commits())
def slugified(request, address, slug): ''' View a component's page with a slugified URL. Checks the slug is correct. ''' component = Component.get( id=None, user=request.user, action=READ, address=address ) # If the slug is correct return the page if slug == component.slug(): return page(request, address, component) # otherwise redirect to canonical URL with the correct slug else: return redirect(component.url(), permanent=True)
def component_forks(request, id): ''' Get a list of a component's forks For: GET /components/{id}/forks ''' api = API(request) component = Component.one_if_authorized_or_raise( user=request.user, action=READ, id=id ) if api.get: forks = component.forks(user) return api.respond(forks) else: api.raise_method_not_allowed()
def commit(request, address): ''' Commit the component This is a temporary stub implementation. It should robably be implemented as a "method" call above. ''' api = API(request) component = Component.get( id=None, user=request.user, action=READ, address=address ) return api.respond(dict( revision='xxxxxxxxx' ))
def deactivate(request, address): ''' Dectivate a component by stopping the currently active session for it ''' api = API(request) if api.put: if not request.user.is_authenticated(): return api.respond_signin() else: component = Component.get( id=None, user=request.user, action=READ, address=address ) session = component.deactivate( user=request.user ) return api.respond(session) return api.respond_bad()
def component_fork(request, id): ''' Fork a component For: POST /components/{id}/fork ''' api = API(request) api.authenticated_or_raise() component = Component.get( id=id, user=request.user, action=READ, ) if api.post: fork = component.fork( user=request.user, to=api.required('to') ) return api.respond(fork) else: api.raise_method_not_allowed()
def session(request, address): ''' Get the session, if any, for a component ''' api = API(request) if api.put: if not request.user.is_authenticated(): return api.respond_signin() else: component = Component.get( id=None, user=request.user, action=READ, address=address ) session = component.session( user=request.user, required=False ) if session and session.active: session.update() return api.respond(session) return api.respond_bad()
def component_star(request, id): ''' Add/remove the component to/from the request user's starred list For: PUT /components/{id}/star DELETE /components/{id}/star ''' api = API(request) api.authenticated_or_raise() component = Component.get( id=id, user=request.user, action=READ ) if api.put: component.star(request.user) elif api.delete: component.unstar(request.user) else: api.raise_method_not_allowed() return api.respond({ 'stars': component.stars, 'starred': component.starred(request.user) })
def git(request, address): ''' Serve a component repository. This view is used by Git clients to clone/pull/push to/from component respositories e.g. using the git shell program: git clone https://stenci.la/some/component e.g. using libgit2 integrated into stencila: stencil.pull() These commands results in requests to .git subdirectory of the component directory. Authentication will normally be done by UserToken via BasicAuth which uses the "Authorization" HTTP header. You can specify token (and no password) like this: git clone http://01sVr0jlc03M4qIBC1MQazbOlZvtcZZT5DkQ3Ir8uhMaEcoZhMo8dhHwvrFlKuVjRO:none@localhost:8010/235.git Note that Git does not supply the "Authorization" header until challenged to authenticate with a 401 header. So, a 401 response at first will be a normal part of the communications with Git for requests requiring UPDATE rights. If auth is not provided in the URL as above then the Git client will prompt for username/password. This method has to be csrf_exempt otherwise Git can not POST to it because it does not have a Django CRSF token. This view authorization and then gets `git-http-backend.go` to handle the request via an Nginx X-Accel-Redirect. `git-http-backend.go` is a Go implementation of the (C CGI backend)[http://git-scm.com/docs/git-http-backend]. It is (should be?) faster and (for me) required less configuration to get working than the C CGI backend. ''' # Authorize action service = request.GET.get('service') if request.method == 'GET': # A push involves a GET of git-receive-pack first. # So require UPDATE for this request if service == 'git-receive-pack': action = UPDATE else: action = READ else: # A pull, clone etc involves a POST to /foo/.git/git-upload-pack # So only require READ for this request if 'git-upload-pack' in request.path: action = READ else: action = UPDATE # If action requires UPDATE then the user must be authenticated first if action == UPDATE and not request.user.is_authenticated(): return unauthenticated_response() # Get component thereby checking access rights Component.get( id=None, user=request.user, action=action, address=address ) # Respond if settings.MODE == 'local': # Redirect to `curator.go` operating on this same machine # It is not possible to redirect to `http://10.0.1.50:7311` (i.e. using # the standard IP for the curator) because libgit2 complains with "Cross host redirect not allowed" # So instead we use `10.0.1.25`, the local IP of this, the director, but the port number # of the curator. url = 'http://10.0.1.25:7311' + request.get_full_path().replace('.git', '/.git') return HttpResponseRedirect(url) else: # Ask Nginx to do a proxy pass. # Replace '<address>.git' with '<address>/.git' url = '/internal-component-git' + request.get_full_path().replace('.git', '/.git') # For debugging purposes show the redirect header in the response content response = django.http.HttpResponse('Internal redirect to : %s' % url) response['X-Accel-Redirect'] = url return response